Reshama commited on
Commit
852dc09
·
verified ·
1 Parent(s): 6a920eb

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +103 -0
  2. llms.py +217 -0
  3. templates/index.html +403 -0
app.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify, session, send_from_directory
2
+ import os
3
+ from llms import prompt_llm_image, prompt_llm, parse_bullet_points
4
+ app = Flask(__name__)
5
+ app.secret_key = "32fe450cc58c8e1487b36b9e7f06d349f67afa85d4a074e151a75e5f39517d0c"
6
+ UPLOAD_FOLDER = "uploads"
7
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
8
+
9
+ @app.route("/")
10
+ def index():
11
+ return render_template("index.html")
12
+
13
+ @app.route("/upload", methods=["POST"])
14
+ def upload_image():
15
+ if "image" not in request.files:
16
+ return jsonify({"error": "No image uploaded"})
17
+ file = request.files["image"]
18
+ if file.filename == "":
19
+ return jsonify({"error": "No image selected"})
20
+ filename = file.filename
21
+ filepath = os.path.join(UPLOAD_FOLDER, filename)
22
+ file.save(filepath)
23
+ # Analyze image with LLM
24
+ image_prompt = """
25
+ You are helpful museum guide who can explain the history of fashion items.
26
+
27
+ Analyze this fashion/clothing image and identify the 5 most interesting items.
28
+
29
+ For EACH item, provide:
30
+ 1. A short name (8 words max)
31
+ 2. The approximate location as a percentage from top-left corner (x, y coordinates where 0,0 is top-left and 100,100 is bottom-right)
32
+
33
+ Format your response EXACTLY like this:
34
+ - Item name | x,y
35
+
36
+ Example:
37
+ - Elegant lace collar detail | 50,20
38
+ - Puffed sleeve with gathered fabric | 20,40
39
+ - Pearl button embellishments | 50,60
40
+
41
+ Give me exactly 5 items in this format.
42
+ """
43
+ response = prompt_llm_image(filepath, image_prompt)
44
+ # Parse items with coordinates
45
+ items_with_coords = []
46
+ for line in response.strip().split('\n'):
47
+ line = line.strip()
48
+ if line.startswith('-') or line.startswith('•'):
49
+ line = line.lstrip('-•').strip()
50
+ if '|' in line:
51
+ parts = line.split('|')
52
+ item_name = parts[0].strip()
53
+ coords = parts[1].strip() if len(parts) > 1 else "50,50"
54
+ items_with_coords.append({"name": item_name, "coords": coords})
55
+
56
+ # Fallback if no coordinates found
57
+ if not items_with_coords:
58
+ bullet_list = parse_bullet_points(response)
59
+ items_with_coords = [{"name": item, "coords": "50,50"} for item in bullet_list]
60
+ session["image_path"] = filepath
61
+ session["items"] = items_with_coords
62
+ return jsonify({
63
+ "success": True,
64
+ "items": items_with_coords,
65
+ "image_filename": filename,
66
+ "raw_response": response
67
+ })
68
+
69
+ @app.route("/explain", methods=["POST"])
70
+ def explain_item():
71
+ data = request.get_json()
72
+ item_index = data.get("item_index")
73
+ if "items" not in session or item_index >= len(session["items"]):
74
+ return jsonify({"error": "Invalid item selection"})
75
+ item_data = session["items"][item_index]
76
+ item = item_data["name"] if isinstance(item_data, dict) else item_data
77
+ history_prompt = f"""
78
+ explain the item: {item}
79
+ * instructions:
80
+ - a short quick history time period of the item
81
+ - make your response structured as follows:
82
+ # History of the item
83
+ - Circa 1890
84
+ # Where is it popular
85
+ - United States
86
+ # What is it made of
87
+ - Gold
88
+ # What is it used for
89
+ - Jewelry
90
+ # Who made it
91
+ - Tiffany & Co.
92
+ # Who owned it
93
+ - John D. Rockefeller
94
+ """
95
+ explanation = prompt_llm(history_prompt)
96
+ return jsonify({"success": True, "item": item, "explanation": explanation})
97
+
98
+ @app.route("/uploads/<filename>")
99
+ def uploaded_file(filename):
100
+ return send_from_directory(UPLOAD_FOLDER, filename)
101
+
102
+ if __name__ == "__main__":
103
+ app.run(debug=True, port=5005)
llms.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import re
4
+ from dotenv import load_dotenv
5
+ from openai import OpenAI
6
+
7
+ # Load environment variables from .env file
8
+ load_dotenv()
9
+
10
+ # Initialize OpenAI client
11
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
12
+
13
+
14
+ def prompt_llm(user_text):
15
+ """
16
+ Basic function to send user text to GPT-4 mini and return the response.
17
+
18
+ Args:
19
+ user_text (str): The input text from the user
20
+
21
+ Returns:
22
+ str: The response text from GPT-4 mini
23
+ """
24
+ try:
25
+ response = client.chat.completions.create(
26
+ model="gpt-4o-mini", # Using GPT-4 mini model
27
+ messages=[{"role": "user", "content": user_text}],
28
+ max_tokens=1000,
29
+ temperature=0.7,
30
+ )
31
+
32
+ return response.choices[0].message.content
33
+
34
+ except Exception as e:
35
+ return f"Error: {str(e)}"
36
+
37
+
38
+ def prompt_llm_image(image_path, prompt_text):
39
+ """
40
+ Function to send an image and text prompt to GPT-4 Vision and return the response.
41
+
42
+ Args:
43
+ image_path (str): Path to the image file
44
+ prompt_text (str): The text prompt to ask about the image
45
+
46
+ Returns:
47
+ str: The response text from GPT-4 Vision
48
+ """
49
+ try:
50
+ # Check if image file exists
51
+ if not os.path.exists(image_path):
52
+ return f"Error: Image file not found at {image_path}"
53
+
54
+ # Read and encode the image
55
+ with open(image_path, "rb") as image_file:
56
+ base64_image = base64.b64encode(image_file.read()).decode("utf-8")
57
+
58
+ # Determine the image format
59
+ image_extension = os.path.splitext(image_path)[1].lower()
60
+ if image_extension in [".jpg", ".jpeg"]:
61
+ image_format = "jpeg"
62
+ elif image_extension == ".png":
63
+ image_format = "png"
64
+ elif image_extension == ".gif":
65
+ image_format = "gif"
66
+ elif image_extension == ".webp":
67
+ image_format = "webp"
68
+ else:
69
+ return f"Error: Unsupported image format {image_extension}. Supported formats: jpg, jpeg, png, gif, webp"
70
+
71
+ response = client.chat.completions.create(
72
+ model="gpt-4o", # Using GPT-4 with vision capabilities
73
+ messages=[
74
+ {
75
+ "role": "user",
76
+ "content": [
77
+ {"type": "text", "text": prompt_text},
78
+ {
79
+ "type": "image_url",
80
+ "image_url": {
81
+ "url": f"data:image/{image_format};base64,{base64_image}"
82
+ },
83
+ },
84
+ ],
85
+ }
86
+ ],
87
+ max_tokens=1000,
88
+ temperature=0.7,
89
+ )
90
+
91
+ return response.choices[0].message.content
92
+
93
+ except Exception as e:
94
+ return f"Error: {str(e)}"
95
+
96
+
97
+ def parse_bullet_points(llm_output):
98
+ """
99
+ Parse LLM output and extract bullet points into a list of individual items.
100
+
101
+ Args:
102
+ llm_output (str): The output text from the LLM containing bullet points
103
+
104
+ Returns:
105
+ list: A list of individual bullet point items (up to 5 items)
106
+ """
107
+ if (
108
+ not llm_output
109
+ or isinstance(llm_output, str)
110
+ and llm_output.startswith("Error:")
111
+ ):
112
+ return []
113
+
114
+ # Split by lines and process each line
115
+ lines = llm_output.strip().split("\n")
116
+ bullet_points = []
117
+
118
+ for line in lines:
119
+ line = line.strip()
120
+
121
+ # Skip empty lines
122
+ if not line:
123
+ continue
124
+
125
+ # Look for bullet points with various formats:
126
+ # - Traditional bullets: -, *, •
127
+ # - Numbered lists: 1., 2., etc.
128
+ # - Unicode bullets: ◦, ▪, ▫, etc.
129
+ bullet_patterns = [
130
+ r"^[-*•◦▪▫]\s*(.+)", # - * • ◦ ▪ ▫ bullets
131
+ r"^\d+\.\s*(.+)", # 1. 2. 3. numbered
132
+ r"^[a-zA-Z]\.\s*(.+)", # a. b. c. lettered
133
+ r"^\d+\)\s*(.+)", # 1) 2) 3) numbered with parenthesis
134
+ ]
135
+
136
+ # Try each pattern
137
+ for pattern in bullet_patterns:
138
+ match = re.match(pattern, line)
139
+ if match:
140
+ bullet_text = match.group(1).strip()
141
+ # Remove any trailing punctuation and clean up
142
+ bullet_text = bullet_text.rstrip(".,;:!?")
143
+ if bullet_text: # Only add non-empty items
144
+ bullet_points.append(bullet_text)
145
+ break
146
+
147
+ # Return up to 5 items as requested
148
+ return bullet_points[:5]
149
+
150
+
151
+ # Example usage
152
+ if __name__ == "__main__":
153
+ # # Test the text function
154
+ # print("=== Testing Text Function ===")
155
+ # user_input = "Hello! Can you explain what machine learning is in simple terms?"
156
+ # response = prompt_llm(user_input)
157
+ # print(f"User: {user_input}")
158
+ # print(f"AI: {response}")
159
+
160
+ print("\n=== Testing Image Function ===")
161
+ # Test the image function (assuming there's an image.jpg in the project)
162
+ image_path = "image.jpg"
163
+ if os.path.exists(image_path):
164
+ # ===============================
165
+ # First LLM ImageSummLLM
166
+ # ===============================
167
+ image_prompt = """
168
+ You are helpful museum guide who can explain the history of different items
169
+
170
+ What do you see in this image?
171
+
172
+ * instructions:
173
+ - give me a bullet point list of the 5 most interesting items
174
+ - each bullet point should be 8 words max
175
+ - there should be 5 bullet points max
176
+ """
177
+ image_response = prompt_llm_image(image_path, image_prompt)
178
+ print(f"Image: {image_path}")
179
+ print(f"Prompt: {image_prompt}")
180
+ print(f"AI Response:\n{image_response}")
181
+
182
+ # Parse the bullet points into a list
183
+ bullet_list = parse_bullet_points(image_response)
184
+ print(f"\nParsed Bullet Points ({len(bullet_list)} items):")
185
+ for i, item in enumerate(bullet_list, 1):
186
+ print(f"{i}. {item}")
187
+
188
+ # ===============================
189
+ # Second LLM HistoryLLM
190
+ # ===============================
191
+ item_number = input("Choose item number to explain: ")
192
+ item = bullet_list[int(item_number) - 1]
193
+ history_prompt = f"""\n
194
+ explain the item: {item}
195
+ * instructions:
196
+ - a short quick history time period of the item
197
+ - make your response structured as follows:
198
+ # History of the item
199
+ - Circa 1890
200
+ # Where is it popular
201
+ - United States
202
+ # What is it made of
203
+ - Gold
204
+ # What is it used for
205
+ - Jewelry
206
+ # Who made it
207
+ - Tiffany & Co.
208
+ # Who owned it
209
+ - John D. Rockefeller
210
+
211
+ """
212
+ item_response = prompt_llm(history_prompt)
213
+ print(f"Item Response:\n{item_response}")
214
+ else:
215
+ print(
216
+ f"No test image found at {image_path}. To test image functionality, add an image file and update the path."
217
+ )
templates/index.html ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ThreadStory - Fashion History Explorer</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ font-family: 'Georgia', serif;
11
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
12
+ min-height: 100vh;
13
+ padding: 20px;
14
+ }
15
+ .container {
16
+ max-width: 1000px;
17
+ margin: 0 auto;
18
+ background: white;
19
+ border-radius: 15px;
20
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
21
+ overflow: hidden;
22
+ }
23
+ .header {
24
+ background: linear-gradient(45deg, #8B4513, #A0522D);
25
+ color: white;
26
+ padding: 30px;
27
+ text-align: center;
28
+ }
29
+ .header h1 { font-size: 2.5rem; margin-bottom: 10px; }
30
+ .content { padding: 30px; }
31
+ .step {
32
+ margin-bottom: 30px;
33
+ padding: 20px;
34
+ border: 2px solid #D2B48C;
35
+ border-radius: 10px;
36
+ background: #f9f9f9;
37
+ }
38
+ .step h2 { color: #8B4513; margin-bottom: 15px; }
39
+ .upload-area {
40
+ border: 2px dashed #8B4513;
41
+ padding: 40px;
42
+ text-align: center;
43
+ border-radius: 10px;
44
+ cursor: pointer;
45
+ transition: background 0.3s;
46
+ }
47
+ .upload-area:hover { background: #f0f0f0; }
48
+ .upload-area.dragover { background: #e8f4f8; }
49
+ input[type="file"] { display: none; }
50
+ .btn {
51
+ background: linear-gradient(45deg, #8B4513, #A0522D);
52
+ color: white;
53
+ padding: 12px 25px;
54
+ border: none;
55
+ border-radius: 25px;
56
+ cursor: pointer;
57
+ font-size: 1rem;
58
+ margin: 10px 5px;
59
+ transition: transform 0.2s;
60
+ }
61
+ .btn:hover { transform: translateY(-2px); }
62
+ .btn:disabled { opacity: 0.6; cursor: not-allowed; }
63
+
64
+ /* Interactive Image Styles */
65
+ .image-container {
66
+ position: relative;
67
+ display: inline-block;
68
+ max-width: 100%;
69
+ margin: 20px auto;
70
+ }
71
+ .interactive-image {
72
+ max-width: 100%;
73
+ height: auto;
74
+ display: block;
75
+ border-radius: 10px;
76
+ }
77
+ .hotspot {
78
+ position: absolute;
79
+ width: 40px;
80
+ height: 40px;
81
+ background: rgba(139, 69, 19, 0.8);
82
+ border: 3px solid white;
83
+ border-radius: 50%;
84
+ cursor: pointer;
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ color: white;
89
+ font-weight: bold;
90
+ font-size: 18px;
91
+ transform: translate(-50%, -50%);
92
+ transition: all 0.3s;
93
+ box-shadow: 0 4px 10px rgba(0,0,0,0.3);
94
+ z-index: 10;
95
+ }
96
+ .hotspot:hover {
97
+ transform: translate(-50%, -50%) scale(1.2);
98
+ background: rgba(160, 82, 45, 0.9);
99
+ }
100
+ .hotspot.active {
101
+ background: rgba(160, 82, 45, 1);
102
+ animation: pulse 1s infinite;
103
+ }
104
+ @keyframes pulse {
105
+ 0%, 100% { transform: translate(-50%, -50%) scale(1); }
106
+ 50% { transform: translate(-50%, -50%) scale(1.15); }
107
+ }
108
+
109
+ .popup-bubble {
110
+ position: fixed;
111
+ background: white;
112
+ border: 3px solid #8B4513;
113
+ border-radius: 15px;
114
+ padding: 15px;
115
+ min-width: 200px;
116
+ max-width: 300px;
117
+ box-shadow: 0 8px 20px rgba(0,0,0,0.2);
118
+ z-index: 1000;
119
+ display: none;
120
+ }
121
+ .popup-bubble.show {
122
+ display: block;
123
+ animation: popIn 0.3s ease-out;
124
+ }
125
+ @keyframes popIn {
126
+ from { opacity: 0; transform: scale(0.8); }
127
+ to { opacity: 1; transform: scale(1); }
128
+ }
129
+ .popup-bubble h4 {
130
+ color: #8B4513;
131
+ margin-bottom: 10px;
132
+ font-size: 1.1rem;
133
+ }
134
+ .popup-bubble p {
135
+ color: #555;
136
+ margin-bottom: 10px;
137
+ font-size: 0.9rem;
138
+ }
139
+ .popup-bubble button {
140
+ width: 100%;
141
+ background: #8B4513;
142
+ color: white;
143
+ border: none;
144
+ padding: 8px;
145
+ border-radius: 8px;
146
+ cursor: pointer;
147
+ font-size: 0.9rem;
148
+ }
149
+ .popup-bubble button:hover {
150
+ background: #A0522D;
151
+ }
152
+ .popup-close {
153
+ position: absolute;
154
+ top: 5px;
155
+ right: 10px;
156
+ background: none;
157
+ border: none;
158
+ font-size: 20px;
159
+ cursor: pointer;
160
+ color: #999;
161
+ width: auto !important;
162
+ padding: 0 !important;
163
+ }
164
+ .popup-close:hover {
165
+ color: #333;
166
+ background: none !important;
167
+ }
168
+
169
+ .explanation {
170
+ background: #f8f9fa;
171
+ padding: 20px;
172
+ border-radius: 10px;
173
+ border-left: 4px solid #8B4513;
174
+ white-space: pre-line;
175
+ line-height: 1.6;
176
+ }
177
+ .loading { text-align: center; color: #8B4513; font-style: italic; }
178
+ .error {
179
+ background: #f8d7da;
180
+ color: #721c24;
181
+ padding: 15px;
182
+ border-radius: 8px;
183
+ border-left: 4px solid #dc3545;
184
+ }
185
+ .hidden { display: none; }
186
+ .center { text-align: center; }
187
+ </style>
188
+ </head>
189
+ <body>
190
+ <div class="container">
191
+ <div class="header">
192
+ <h1>🧵 ThreadStory</h1>
193
+ <p>Upload an image to discover the history of fashion items</p>
194
+ </div>
195
+
196
+ <div class="content">
197
+ <!-- Step 1: Upload Image -->
198
+ <div class="step" id="step1">
199
+ <h2>Step 1: Upload Image</h2>
200
+ <div class="upload-area" onclick="document.getElementById('imageInput').click()">
201
+ <p>📸 Click here or drag & drop an image</p>
202
+ <input type="file" id="imageInput" accept="image/*" onchange="uploadImage()">
203
+ </div>
204
+ <div id="uploadStatus"></div>
205
+ </div>
206
+
207
+ <!-- Step 2: Interactive Image with Hotspots -->
208
+ <div class="step hidden" id="step2">
209
+ <h2>Step 2: Click on Items in the Image</h2>
210
+ <p>Click on the numbered circles to learn about each fashion item:</p>
211
+ <div class="center">
212
+ <div class="image-container" id="imageContainer">
213
+ <img id="uploadedImage" class="interactive-image" src="" alt="Uploaded fashion item">
214
+ <!-- Hotspots will be added here dynamically -->
215
+ </div>
216
+ </div>
217
+ <!-- Popup bubble template -->
218
+ <div class="popup-bubble" id="popupBubble">
219
+ <button class="popup-close" onclick="closePopup()">×</button>
220
+ <h4 id="popupTitle"></h4>
221
+ <p id="popupDescription">Click "Learn More" for detailed history</p>
222
+ <button onclick="learnMore()">Learn More</button>
223
+ </div>
224
+ </div>
225
+
226
+ <!-- Step 3: View Detailed Explanation -->
227
+ <div class="step hidden" id="step3">
228
+ <h2>Step 3: Historical Explanation</h2>
229
+ <div id="explanation"></div>
230
+ <button class="btn" onclick="goBackToImage()">← Back to Image</button>
231
+ </div>
232
+ </div>
233
+ </div>
234
+
235
+ <script>
236
+ let currentItems = [];
237
+ let currentItemIndex = null;
238
+ let imageFilename = '';
239
+
240
+ function uploadImage() {
241
+ const file = document.getElementById('imageInput').files[0];
242
+ if (!file) return;
243
+
244
+ const status = document.getElementById('uploadStatus');
245
+ status.innerHTML = '<div class="loading">Analyzing image and detecting items...</div>';
246
+
247
+ const formData = new FormData();
248
+ formData.append('image', file);
249
+
250
+ fetch('/upload', {
251
+ method: 'POST',
252
+ body: formData
253
+ })
254
+ .then(response => response.json())
255
+ .then(data => {
256
+ if (data.success) {
257
+ currentItems = data.items;
258
+ imageFilename = data.image_filename;
259
+ displayInteractiveImage();
260
+ document.getElementById('step1').classList.add('hidden');
261
+ document.getElementById('step2').classList.remove('hidden');
262
+ } else {
263
+ status.innerHTML = `<div class="error">${data.error}</div>`;
264
+ }
265
+ })
266
+ .catch(error => {
267
+ status.innerHTML = '<div class="error">Error uploading image</div>';
268
+ });
269
+ }
270
+
271
+ function displayInteractiveImage() {
272
+ const img = document.getElementById('uploadedImage');
273
+ const container = document.getElementById('imageContainer');
274
+
275
+ // Set image source
276
+ img.src = `/uploads/${imageFilename}`;
277
+
278
+ // Wait for image to load before adding hotspots
279
+ img.onload = function() {
280
+ // Remove any existing hotspots
281
+ const existingHotspots = container.querySelectorAll('.hotspot');
282
+ existingHotspots.forEach(h => h.remove());
283
+
284
+ // Add hotspots for each item
285
+ currentItems.forEach((item, index) => {
286
+ const coords = item.coords.split(',');
287
+ const x = parseFloat(coords[0]);
288
+ const y = parseFloat(coords[1]);
289
+
290
+ const hotspot = document.createElement('div');
291
+ hotspot.className = 'hotspot';
292
+ hotspot.textContent = index + 1;
293
+ hotspot.style.left = x + '%';
294
+ hotspot.style.top = y + '%';
295
+ hotspot.onclick = (e) => showPopup(index, e);
296
+
297
+ container.appendChild(hotspot);
298
+ });
299
+ };
300
+ }
301
+
302
+ function showPopup(itemIndex, event) {
303
+ currentItemIndex = itemIndex;
304
+ const item = currentItems[itemIndex];
305
+ const popup = document.getElementById('popupBubble');
306
+ const title = document.getElementById('popupTitle');
307
+
308
+ title.textContent = item.name;
309
+
310
+ // Position popup near the clicked hotspot
311
+ const container = document.getElementById('imageContainer');
312
+ const containerRect = container.getBoundingClientRect();
313
+ const hotspot = event.target;
314
+ const hotspotRect = hotspot.getBoundingClientRect();
315
+
316
+ // Position popup near the clicked hotspot (using fixed positioning)
317
+ const x = hotspotRect.left + hotspotRect.width / 2;
318
+ const y = hotspotRect.top - 20;
319
+
320
+ popup.style.left = x + 'px';
321
+ popup.style.top = y + 'px';
322
+ popup.style.transform = 'translate(-50%, -100%)';
323
+ popup.classList.add('show');
324
+
325
+ // Highlight active hotspot
326
+ document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active'));
327
+ hotspot.classList.add('active');
328
+ }
329
+
330
+ function closePopup() {
331
+ document.getElementById('popupBubble').classList.remove('show');
332
+ document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active'));
333
+ }
334
+
335
+ function learnMore() {
336
+ closePopup();
337
+ const explanation = document.getElementById('explanation');
338
+ explanation.innerHTML = '<div class="loading">Getting detailed historical explanation...</div>';
339
+
340
+ fetch('/explain', {
341
+ method: 'POST',
342
+ headers: { 'Content-Type': 'application/json' },
343
+ body: JSON.stringify({ item_index: currentItemIndex })
344
+ })
345
+ .then(response => response.json())
346
+ .then(data => {
347
+ if (data.success) {
348
+ explanation.innerHTML = `
349
+ <h3>${data.item}</h3>
350
+ <div class="explanation">${data.explanation}</div>
351
+ `;
352
+ document.getElementById('step2').classList.add('hidden');
353
+ document.getElementById('step3').classList.remove('hidden');
354
+ } else {
355
+ explanation.innerHTML = `<div class="error">${data.error}</div>`;
356
+ }
357
+ })
358
+ .catch(error => {
359
+ explanation.innerHTML = '<div class="error">Error getting explanation</div>';
360
+ });
361
+ }
362
+
363
+ function goBackToImage() {
364
+ document.getElementById('step3').classList.add('hidden');
365
+ document.getElementById('step2').classList.remove('hidden');
366
+ }
367
+
368
+ // Drag and drop functionality
369
+ const uploadArea = document.querySelector('.upload-area');
370
+ uploadArea.addEventListener('dragover', (e) => {
371
+ e.preventDefault();
372
+ uploadArea.classList.add('dragover');
373
+ });
374
+ uploadArea.addEventListener('dragleave', () => {
375
+ uploadArea.classList.remove('dragover');
376
+ });
377
+ uploadArea.addEventListener('drop', (e) => {
378
+ e.preventDefault();
379
+ uploadArea.classList.remove('dragover');
380
+ const files = e.dataTransfer.files;
381
+ if (files.length > 0) {
382
+ document.getElementById('imageInput').files = files;
383
+ uploadImage();
384
+ }
385
+ });
386
+
387
+ // Close popup when clicking outside
388
+ document.addEventListener('click', (e) => {
389
+ const popup = document.getElementById('popupBubble');
390
+ const hotspots = document.querySelectorAll('.hotspot');
391
+ let clickedHotspot = false;
392
+
393
+ hotspots.forEach(h => {
394
+ if (h.contains(e.target)) clickedHotspot = true;
395
+ });
396
+
397
+ if (!popup.contains(e.target) && !clickedHotspot && popup.classList.contains('show')) {
398
+ closePopup();
399
+ }
400
+ });
401
+ </script>
402
+ </body>
403
+ </html>