rairo commited on
Commit
275e65d
Β·
verified Β·
1 Parent(s): e779583

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +238 -379
app.py CHANGED
@@ -17,63 +17,48 @@ os.environ["STREAMLIT_ANALYTICS_ENABLED"] = "false"
17
  # 1.1 Load your Google API key from environment or Streamlit secrets
18
  try:
19
  API_KEY = st.secrets["GOOGLE_API_KEY"]
20
- except:
21
  API_KEY = os.environ.get("GOOGLE_API_KEY")
22
 
23
  if not API_KEY:
24
  st.error("Please set GOOGLE_API_KEY in your environment variables or Streamlit secrets")
25
  st.stop()
26
 
27
- # 1.2 Initialize the GenAI client
28
- client = genai.Client(api_key=API_KEY)
 
 
 
 
29
 
30
- # 1.3 Constants (model IDs)
31
  CATEGORY_MODEL = "gemini-2.0-flash-exp"
32
  GENERATION_MODEL = "gemini-2.0-flash-exp-image-generation"
33
 
34
  # 1.4 Helper to parse numbered steps out of Gemini text
35
  def parse_numbered_steps(text):
36
  """
37
- Expect Gemini to return something like:
38
- 1. First step description...
39
- 2. Second step description...
40
- ...
41
- Returns a list of (step_number, step_text).
42
  """
43
- raw_steps = re.split(r"\n\s*\d+\.\s+", text.strip())
44
- if raw_steps and raw_steps[0].strip() == "":
45
- raw_steps.pop(0)
46
- steps = []
47
- for idx, chunk in enumerate(raw_steps, start=1):
48
- steps.append((idx, chunk.strip()))
49
- return steps
50
 
51
  # ─────────────────────────────────────────────────────────────────────────────
52
  # 2. SESSION STATE SETUP
53
  # ─────────────────────────────────────────────────────────────────────────────
54
 
55
- if "steps" not in st.session_state:
56
- st.session_state.steps = [] # List[(int, str)]
57
- if "images" not in st.session_state:
58
- st.session_state.images = {} # Dict[int, PIL.Image] (illustrations per step)
59
- if "tools_list" not in st.session_state:
60
- st.session_state.tools_list = [] # List[str]
61
- if "current_step" not in st.session_state:
62
- st.session_state.current_step = 1
63
- if "done_flags" not in st.session_state:
64
- st.session_state.done_flags = {} # Dict[int, bool]
65
- if "notes" not in st.session_state:
66
- st.session_state.notes = {} # Dict[int, str]
67
- if "timers" not in st.session_state:
68
- st.session_state.timers = {} # Dict[int, int]
69
- if "category" not in st.session_state:
70
- st.session_state.category = None
71
- if "prompt_sent" not in st.session_state:
72
- st.session_state.prompt_sent = False
73
- if "timer_running" not in st.session_state:
74
- st.session_state.timer_running = {}
75
- if "last_tick" not in st.session_state:
76
- st.session_state.last_tick = {}
77
 
78
  # ─────────────────────────────────────────────────────────────────────────────
79
  # 3. LAYOUT & FUNCTIONS
@@ -81,291 +66,181 @@ if "last_tick" not in st.session_state:
81
 
82
  def reset_state():
83
  """Clear out all session state so user can start fresh."""
84
- st.session_state.steps = []
85
- st.session_state.images = {}
86
- st.session_state.tools_list = []
87
- st.session_state.current_step = 1
88
- st.session_state.done_flags = {}
89
- st.session_state.notes = {}
90
- st.session_state.timers = {}
91
- st.session_state.category = None
92
- st.session_state.prompt_sent = False
93
- st.session_state.timer_running = {}
94
- st.session_state.last_tick = {}
95
-
96
- def detect_category_and_generate(uploaded_file, context_text):
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  """
98
- 1) Use Gemini chat to identify the category (appliance, automotive, gardening, upcycling).
99
- 2) Then use Gemini chat to get:
100
- - a "tools & materials" list
101
- - numbered step-by-step instructions (and optional illustrations)
102
- 3) Parse everything into session_state.
103
  """
104
- try:
105
- # Load the uploaded image as a PIL.Image
106
- image = Image.open(uploaded_file)
107
 
108
- # ─── 3.1 IDENTIFY CATEGORY ─────────────────────────────────────────────────
 
109
  category_prompt = (
110
- "You are an expert DIY assistant. "
111
- "Here is a single user-uploaded image. "
112
- "Determine if it belongs to one of these categories (exactly): "
113
- "home appliance repair, automotive maintenance, gardening & urban farming, or upcycling & sustainable crafts. "
114
- "Reply back with exactly one category name."
115
- )
116
- cat_chat = client.chats.create(
117
- model=CATEGORY_MODEL,
118
- config=types.GenerateContentConfig(response_modalities=["Text", "Image"])
119
  )
120
- cat_resp = cat_chat.send_message([category_prompt, image])
121
-
122
- # Extract the category name (combine all text parts)
123
- cat_parts = cat_resp.candidates[0].content.parts
124
- cat_text = ""
125
- for part in cat_parts:
126
- if part.text is not None:
127
- cat_text += part.text
128
- cat_text = cat_text.strip()
129
- st.session_state.category = cat_text
130
-
131
- # ─── 3.2 REQUEST TOOLS + NUMBERED STEPS ────────────────────────────────────
132
- detailed_prompt = (
133
- f"Category: {cat_text}\n"
134
- f"Context: {context_text}\n\n"
135
- "Provide a detailed repair/maintenance guide with the following format:\n\n"
136
- "TOOLS AND MATERIALS:\n"
137
- "- Tool A\n"
138
- "- Material B\n"
139
- "- Part C\n\n"
140
- "STEPS:\n"
141
- "1. First step instructions (be specific and detailed)...\n"
142
- "2. Second step instructions...\n"
143
- "3. Continue with all necessary steps...\n\n"
144
- "Additionally, for each step you may include a brief illustrative image. "
145
- "Keep each numbered step clear and actionable (2-3 sentences max). "
146
- "Include safety warnings where appropriate."
147
- )
148
- gen_chat = client.chats.create(
149
- model=GENERATION_MODEL,
150
- config=types.GenerateContentConfig(response_modalities=["Text", "Image"])
151
- )
152
- full_resp = gen_chat.send_message([detailed_prompt, image])
153
-
154
- # ─── 3.3 PARSE out tools, numbered steps, and images ────────────────────────
155
- gen_parts = full_resp.candidates[0].content.parts
156
-
157
- # First, gather all text into one string to parse tools/steps
158
- combined_text = ""
159
- for part in gen_parts:
160
- if part.text is not None:
161
- combined_text += part.text + "\n"
162
- combined_text = combined_text.strip()
163
-
164
- # Now identify and extract any inline images. We'll assign them sequentially to steps.
165
- inline_images = []
166
- for part in gen_parts:
167
- if part.inline_data is not None:
168
- img = Image.open(BytesIO(part.inline_data.data))
169
- inline_images.append(img)
170
-
171
- # Split combined_text into tools and steps sections
172
- tools_section = ""
173
- steps_section = ""
174
- if "TOOLS AND MATERIALS:" in combined_text:
175
- parts = combined_text.split("TOOLS AND MATERIALS:")
176
- if len(parts) > 1:
177
- remaining = parts[1]
178
- if "STEPS:" in remaining:
179
- tools_part, steps_part = remaining.split("STEPS:", 1)
180
- tools_section = tools_part.strip()
181
- steps_section = steps_part.strip()
182
- else:
183
- tools_section = remaining.strip()
184
- elif "STEPS:" in combined_text:
185
- parts = combined_text.split("STEPS:")
186
- if len(parts) > 1:
187
- steps_section = parts[1].strip()
188
  else:
189
- # Fallback parsing
190
- lines = combined_text.split("\n")
191
- tools_lines = []
192
- steps_lines = []
193
- current_section = "unknown"
194
- for line in lines:
195
- line = line.strip()
196
- if line.startswith("-") and current_section != "steps":
197
- current_section = "tools"
198
- tools_lines.append(line)
199
- elif re.match(r"^\d+\.", line):
200
- current_section = "steps"
201
- steps_lines.append(line)
202
- elif current_section == "steps" and line:
203
- steps_lines.append(line)
204
- tools_section = "\n".join(tools_lines)
205
- steps_section = "\n".join(steps_lines)
206
-
207
- # Parse tools
208
- tools = []
209
- for line in tools_section.split("\n"):
210
- line = line.strip()
211
- if line.startswith("-"):
212
- tools.append(line.lstrip("- ").strip())
213
-
214
- # Parse steps
215
- parsed_steps = parse_numbered_steps(steps_section)
216
-
217
- # Assign inline_images to steps (one-to-one, up to number of steps)
218
- st.session_state.images = {}
219
- for idx, step in parsed_steps:
220
- img_index = idx - 1 # zero-based
221
- if img_index < len(inline_images):
222
- st.session_state.images[idx] = inline_images[img_index]
223
-
224
- # Store tools and steps in session_state
225
- st.session_state.tools_list = tools
226
- st.session_state.steps = parsed_steps
227
-
228
- # Initialize done flags / timers / notes
229
- for idx, step_text in parsed_steps:
230
- st.session_state.done_flags[idx] = False
231
- st.session_state.notes[idx] = ""
232
- timer_match = re.search(r"wait\s+(\d+)\s*(seconds?|minutes?)", step_text.lower())
233
- if timer_match:
234
- val = int(timer_match.group(1))
235
- unit = timer_match.group(2)
236
- seconds_left = val * (60 if "minute" in unit else 1)
237
- st.session_state.timers[idx] = seconds_left
238
- else:
239
- st.session_state.timers[idx] = 0
240
 
241
- st.session_state.prompt_sent = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
- except Exception as e:
244
- st.error(f"Error processing request: {str(e)}")
245
- st.error("Please check your API key and try again.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
 
248
  def render_sidebar_navigation():
249
- """
250
- A sidebar listing each step by number + a button to jump to that step.
251
- Also show overall progress.
252
- """
253
  st.sidebar.markdown("## Steps Navigation")
254
- total_steps = len(st.session_state.steps)
255
- if total_steps > 0:
256
- completed = sum(1 for done in st.session_state.done_flags.values() if done)
257
- st.sidebar.progress(completed / total_steps)
258
- st.sidebar.write(f"Progress: {completed}/{total_steps} steps")
259
-
260
- for (idx, _) in st.session_state.steps:
261
- is_done = st.session_state.done_flags.get(idx, False)
262
  label = f"{'βœ“' if is_done else 'Β·'} Step {idx}"
263
  if st.sidebar.button(label, key=f"nav_{idx}"):
264
- st.session_state.current_step = idx
265
  st.rerun()
266
 
267
-
268
  def render_tools_list():
269
- """Show the Tools/Materials list in an expander."""
270
- if st.session_state.tools_list:
271
- with st.expander("πŸ”§ Required Tools & Materials", expanded=False):
272
- for item in st.session_state.tools_list:
273
- st.write(f"- {item}")
274
-
275
 
276
  def render_step(idx, text):
277
- """
278
- Render a single step:
279
- - Show step number
280
- - Show instruction text
281
- - Possibly show an illustration if available
282
- - Timer if needed
283
- - Checkbox for "Done"
284
- - Text area for notes
285
- - Photo upload
286
- - Prev/Next buttons
287
- """
288
- total = len(st.session_state.steps)
289
  st.markdown(f"### Step {idx} of {total}")
290
  st.write(text)
291
-
292
- # If an illustration exists for this step, display it
293
- if idx in st.session_state.images:
294
- st.image(
295
- st.session_state.images[idx],
296
- caption=f"Illustration for step {idx}",
297
- use_container_width=True
298
- )
299
-
300
- # Timer functionality
301
- seconds_left = st.session_state.timers.get(idx, 0)
302
- if seconds_left > 0:
303
- if idx not in st.session_state.timer_running:
304
- st.session_state.timer_running[idx] = False
305
-
306
- if not st.session_state.timer_running[idx]:
307
- mins = seconds_left // 60
308
- secs = seconds_left % 60
309
- if st.button(f"⏱️ Start timer ({mins}m {secs}s)", key=f"start_{idx}"):
310
- st.session_state.timer_running[idx] = True
311
- st.session_state.last_tick[idx] = time.time()
312
- st.rerun()
313
- else:
314
- if idx in st.session_state.last_tick:
315
- now = time.time()
316
- elapsed = int(now - st.session_state.last_tick[idx])
317
- if elapsed >= 1:
318
- st.session_state.timers[idx] = max(0, st.session_state.timers[idx] - elapsed)
319
- st.session_state.last_tick[idx] = now
320
-
321
- remaining = st.session_state.timers[idx]
322
- if remaining > 0:
323
- mins = remaining // 60
324
- secs = remaining % 60
325
- st.metric("⏲️ Timer", f"{mins:02d}:{secs:02d}")
326
- time.sleep(1)
327
- st.rerun()
328
- else:
329
- st.success("⏰ Timer completed!")
330
- st.session_state.timer_running[idx] = False
331
-
332
- # Done checkbox
333
- done = st.checkbox(
334
- "βœ… Mark this step as completed",
335
- value=st.session_state.done_flags.get(idx, False),
336
- key=f"done_{idx}"
337
- )
338
- st.session_state.done_flags[idx] = done
339
-
340
- # Notes section
341
- notes = st.text_area(
342
- "πŸ“ Your notes for this step:",
343
- value=st.session_state.notes.get(idx, ""),
344
- height=100,
345
- key=f"notes_{idx}"
346
- )
347
- st.session_state.notes[idx] = notes
348
-
349
- # Photo upload
350
- photo = st.file_uploader(
351
- "πŸ“· Upload a progress photo (optional)",
352
- type=["jpg", "jpeg", "png"],
353
- key=f"photo_{idx}"
354
- )
355
- if photo:
356
- st.image(Image.open(photo), caption=f"Progress photo for step {idx}", use_container_width=True)
357
-
358
- # Navigation buttons
359
  st.markdown("---")
360
  col1, col2, col3 = st.columns([1, 2, 1])
361
- with col1:
362
- if idx > 1 and st.button("⬅️ Previous", key=f"prev_{idx}"):
363
- st.session_state.current_step = idx - 1
364
- st.rerun()
365
- with col3:
366
- if idx < total and st.button("Next ➑️", key=f"next_{idx}"):
367
- st.session_state.current_step = idx + 1
368
- st.rerun()
369
 
370
  # ─────────────────────────────────────────────────────────────────────────────
371
  # 4. APP LAYOUT
@@ -375,91 +250,75 @@ st.set_page_config(page_title="NeoFix DIY Assistant", page_icon="πŸ› οΈ", layou
375
  st.title("πŸ› οΈ NeoFix AI-Powered DIY Assistant")
376
 
377
  with st.expander("ℹ️ How it works", expanded=False):
378
- st.write(
379
- """
380
- 1. **Upload a photo** of the item you want to fix or build (appliance, car part, plant, craft project).
381
- 2. **Add context** (optional) - describe what’s wrong or what you want to achieve.
382
- 3. **Get AI guidance** - The AI will detect the category and provide step-by-step instructions (with possible illustrations).
383
- 4. **Follow the steps** - Each step includes:
384
- - Clear instructions
385
- - An illustrative image (if provided)
386
- - Progress tracking with checkboxes
387
- - Timer functionality for waiting periods
388
- - Note-taking area
389
- - Photo upload for progress tracking
390
- 5. **Navigate easily** - Use the sidebar to jump between steps and track overall progress.
391
- """
392
- )
393
-
394
- # Main upload section
395
- st.markdown("---")
396
- col1, col2 = st.columns([3, 1])
397
- with col1:
398
- uploaded_image = st.file_uploader(
399
- "πŸ“· Upload a photo of your project",
400
- type=["jpg", "jpeg", "png"],
401
- help="Supported formats: JPG, JPEG, PNG"
402
- )
403
- context_text = st.text_area(
404
- "✏️ Describe the issue or your goal (optional)",
405
- height=80,
406
- placeholder="e.g., 'My toaster won’t turn on' or 'I want to turn this into a planter'"
407
- )
408
- with col2:
409
- st.markdown("### Actions")
410
- if st.button("πŸš€ Get AI Guidance", type="primary", use_container_width=True):
411
- if not uploaded_image:
412
- st.warning("⚠️ Please upload an image first!")
413
- else:
414
- with st.spinner("πŸ€– Analyzing your image and generating instructions..."):
415
- detect_category_and_generate(uploaded_image, context_text)
416
-
417
  if st.button("πŸ”„ Start Over", use_container_width=True):
418
  reset_state()
419
- st.success("βœ… Reset complete!")
420
- st.rerun()
421
 
422
- # Show the tutorial interface if steps have been generated
423
- if st.session_state.prompt_sent:
424
- # Sidebar navigation
425
  render_sidebar_navigation()
426
-
427
- # Main content area
428
  st.markdown("---")
429
-
430
- # Show detected category
431
- if st.session_state.category:
432
- st.markdown(f"### πŸ” Detected Category: **{st.session_state.category}**")
433
-
434
- # Show tools list
435
- render_tools_list()
436
  st.markdown("---")
437
 
438
- # Show current step
439
- if st.session_state.steps:
440
- max_step = len(st.session_state.steps)
441
- if st.session_state.current_step > max_step:
442
- st.session_state.current_step = max_step
443
- elif st.session_state.current_step < 1:
444
- st.session_state.current_step = 1
445
-
446
- step_num, step_text = st.session_state.steps[st.session_state.current_step - 1]
447
- render_step(step_num, step_text)
448
-
449
- # Overall progress at bottom
450
- st.markdown("---")
451
- total_steps = len(st.session_state.steps)
452
- done_count = sum(1 for d in st.session_state.done_flags.values() if d)
453
- progress = done_count / total_steps if total_steps > 0 else 0
454
- st.progress(progress)
455
- st.markdown(f"**Overall Progress:** {done_count} of {total_steps} steps completed ({progress:.0%})")
456
-
457
- if done_count == total_steps:
458
- st.balloons()
459
- st.success("πŸŽ‰ Congratulations! You've completed all steps!")
460
  else:
461
- st.error("No steps generated. Please try uploading a different image.")
 
 
 
 
 
 
 
 
462
 
463
- # Footer
464
- st.markdown("---")
465
- st.markdown("*Powered by Google Gemini AI - Your intelligent DIY companion*")
 
 
 
 
 
 
 
 
 
 
17
  # 1.1 Load your Google API key from environment or Streamlit secrets
18
  try:
19
  API_KEY = st.secrets["GOOGLE_API_KEY"]
20
+ except (AttributeError, KeyError):
21
  API_KEY = os.environ.get("GOOGLE_API_KEY")
22
 
23
  if not API_KEY:
24
  st.error("Please set GOOGLE_API_KEY in your environment variables or Streamlit secrets")
25
  st.stop()
26
 
27
+ # 1.2 Initialize the GenAI client (as per original code)
28
+ try:
29
+ client = genai.Client(api_key=API_KEY)
30
+ except Exception as e:
31
+ st.error(f"Failed to initialize GenAI Client: {e}")
32
+ st.stop()
33
 
34
+ # 1.3 Constants (model IDs, exactly as in original code)
35
  CATEGORY_MODEL = "gemini-2.0-flash-exp"
36
  GENERATION_MODEL = "gemini-2.0-flash-exp-image-generation"
37
 
38
  # 1.4 Helper to parse numbered steps out of Gemini text
39
  def parse_numbered_steps(text):
40
  """
41
+ Parses text with numbered steps into a list of tuples.
42
+ Example: "1. Do this.\n2. Do that." -> [(1, "Do this."), (2, "Do that.")]
 
 
 
43
  """
44
+ # Add a leading newline to help regex find the first step
45
+ text = "\n" + text
46
+ steps = re.findall(r"\n\s*(\d+)\.\s*(.*)", text, re.MULTILINE)
47
+ return [(int(num), desc.strip()) for num, desc in steps]
 
 
 
48
 
49
  # ─────────────────────────────────────────────────────────────────────────────
50
  # 2. SESSION STATE SETUP
51
  # ─────────────────────────────────────────────────────────────────────────────
52
 
53
+ # Central dictionary for session state management
54
+ if "app_state" not in st.session_state:
55
+ st.session_state.app_state = {
56
+ "steps": [], "images": {}, "tools_list": [], "current_step": 1,
57
+ "done_flags": {}, "notes": {}, "timers": {}, "category": None,
58
+ "prompt_sent": False, "timer_running": {}, "last_tick": {},
59
+ "project_title": "", "project_description": "", "upcycling_options": [],
60
+ "plan_approved": False, "initial_plan": "", "user_image": None
61
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  # ─────────────────────────────────────────────────────────────────────────────
64
  # 3. LAYOUT & FUNCTIONS
 
66
 
67
  def reset_state():
68
  """Clear out all session state so user can start fresh."""
69
+ st.session_state.app_state = {
70
+ "steps": [], "images": {}, "tools_list": [], "current_step": 1,
71
+ "done_flags": {}, "notes": {}, "timers": {}, "category": None,
72
+ "prompt_sent": False, "timer_running": {}, "last_tick": {},
73
+ "project_title": "", "project_description": "", "upcycling_options": [],
74
+ "plan_approved": False, "initial_plan": "", "user_image": None
75
+ }
76
+ st.success("βœ… Reset complete!")
77
+ st.rerun()
78
+
79
+ def send_gemini_request(model_name, prompt, image):
80
+ """Helper to send requests using the client.chats interface."""
81
+ try:
82
+ chat = client.chats.create(
83
+ model=model_name,
84
+ config=types.GenerateContentConfig(response_modalities=["Text"]) # Assuming text response for these tasks
85
+ )
86
+ response = chat.send_message([prompt, image])
87
+ # Combine all text parts from the response
88
+ response_text = "".join(part.text for part in response.candidates[0].content.parts if part.text)
89
+ return response_text.strip()
90
+ except Exception as e:
91
+ st.error(f"Error communicating with model {model_name}: {str(e)}")
92
+ return None
93
+
94
+ def initial_analysis(uploaded_file, context_text):
95
  """
96
+ First pass with AI: get category, then get title, description, and initial plan.
 
 
 
 
97
  """
98
+ image = Image.open(uploaded_file)
99
+ st.session_state.app_state['user_image'] = image
 
100
 
101
+ with st.spinner("πŸ€– Analyzing your project and preparing a plan..."):
102
+ # Step 1: Detect Category using CATEGORY_MODEL
103
  category_prompt = (
104
+ "You are an expert DIY assistant. Analyze the user's image and context. "
105
+ f"Context: '{context_text}'. "
106
+ "Categorize the project into ONE of the following: "
107
+ "Home Appliance Repair, Automotive Maintenance, Gardening & Urban Farming, "
108
+ "Upcycling & Sustainable Crafts, or DIY Project Creation. "
109
+ "Reply with ONLY the category name."
 
 
 
110
  )
111
+ category = send_gemini_request(CATEGORY_MODEL, category_prompt, image)
112
+ if not category: return
113
+ st.session_state.app_state['category'] = category
114
+
115
+ # Step 2: Generate Title, Description, and Plan using GENERATION_MODEL
116
+ plan_prompt = f"""
117
+ You are an expert DIY assistant. The user's project is in the category: {category}.
118
+ User Context: "{context_text if context_text else 'No context provided.'}"
119
+
120
+ Based on the image and context, perform the following:
121
+ 1. **Title:** Create a short, clear title for this project.
122
+ 2. **Description:** Write a brief, one-paragraph description of the goal.
123
+ 3. **Initial Plan:**
124
+ - If the category is 'Upcycling & Sustainable Crafts' AND the user has NOT specified a project, propose three distinct project options as a numbered list under the heading "UPCYCLING OPTIONS:".
125
+ - For all other cases, briefly outline the main stages of the proposed solution.
126
+
127
+ Structure your response EXACTLY like this:
128
+ TITLE: [Your title]
129
+ DESCRIPTION: [Your description]
130
+ INITIAL PLAN:
131
+ [Your plan or 3 options]
132
+ """
133
+ plan_response = send_gemini_request(GENERATION_MODEL, plan_prompt, image)
134
+ if not plan_response: return
135
+
136
+ # Parse the second response
137
+ try:
138
+ st.session_state.app_state['project_title'] = re.search(r"TITLE:\s*(.*)", plan_response).group(1).strip()
139
+ st.session_state.app_state['project_description'] = re.search(r"DESCRIPTION:\s*(.*)", plan_response, re.DOTALL).group(1).strip()
140
+ initial_plan_text = re.search(r"INITIAL PLAN:\s*(.*)", plan_response, re.DOTALL).group(1).strip()
141
+
142
+ if "UPCYCLING OPTIONS:" in initial_plan_text:
143
+ options = re.findall(r"^\s*\d+\.\s*(.*)", initial_plan_text, re.MULTILINE)
144
+ st.session_state.app_state['upcycling_options'] = options
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  else:
146
+ st.session_state.app_state['initial_plan'] = initial_plan_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ st.session_state.app_state['prompt_sent'] = True
149
+ if context_text:
150
+ st.session_state.app_state['plan_approved'] = True
151
+ generate_detailed_steps()
152
+ else:
153
+ st.session_state.app_state['plan_approved'] = False
154
+ except AttributeError:
155
+ st.error("The AI response was not in the expected format. Please try again.")
156
+ st.session_state.app_state['prompt_sent'] = False
157
+
158
+
159
+ def generate_detailed_steps(selected_option=None):
160
+ """Generates the detailed, step-by-step guide using GENERATION_MODEL."""
161
+ image = st.session_state.app_state.get('user_image')
162
+ if not image:
163
+ st.error("Image not found. Please start over."); return
164
+
165
+ context = f"The user has approved the plan for '{st.session_state.app_state['project_title']}'."
166
+ if selected_option:
167
+ context = f"The user chose the upcycling project: '{selected_option}'."
168
+
169
+ detailed_prompt = f"""
170
+ You are a DIY expert. The user wants to proceed with the project titled "{st.session_state.app_state['project_title']}".
171
+ {context}
172
+ Provide a detailed guide in this EXACT format:
173
+
174
+ TOOLS AND MATERIALS:
175
+ - Tool A
176
+ - Material B
177
+
178
+ STEPS:
179
+ 1. First step instructions. Be specific and clear.
180
+ 2. Second step instructions...
181
+ """
182
+ with st.spinner("πŸ› οΈ Generating your detailed step-by-step guide..."):
183
+ full_resp = send_gemini_request(GENERATION_MODEL, detailed_prompt, image)
184
+ if not full_resp: return
185
 
186
+ try:
187
+ tools_section = re.search(r"TOOLS AND MATERIALS:\s*(.*?)\s*STEPS:", full_resp, re.DOTALL).group(1).strip()
188
+ steps_section = re.search(r"STEPS:\s*(.*)", full_resp, re.DOTALL).group(1).strip()
189
+
190
+ st.session_state.app_state['tools_list'] = [line.strip("- ").strip() for line in tools_section.split('\n') if line.strip()]
191
+ st.session_state.app_state['steps'] = parse_numbered_steps(steps_section)
192
+
193
+ for idx, step_text in st.session_state.app_state['steps']:
194
+ st.session_state.app_state['done_flags'][idx] = False
195
+ st.session_state.app_state['notes'][idx] = ""
196
+ timer_match = re.search(r"wait\s+for\s+(\d+)\s+(seconds?|minutes?)", step_text.lower())
197
+ if timer_match:
198
+ val, unit = int(timer_match.group(1)), timer_match.group(2)
199
+ st.session_state.app_state['timers'][idx] = val * (60 if "minute" in unit else 1)
200
+ else:
201
+ st.session_state.app_state['timers'][idx] = 0
202
+ except AttributeError:
203
+ st.error("Failed to parse the detailed guide from the AI. The format was incorrect.")
204
 
205
 
206
  def render_sidebar_navigation():
 
 
 
 
207
  st.sidebar.markdown("## Steps Navigation")
208
+ steps = st.session_state.app_state['steps']
209
+ if not steps: return
210
+ total_steps = len(steps)
211
+ completed = sum(1 for done in st.session_state.app_state['done_flags'].values() if done)
212
+ st.sidebar.progress(completed / total_steps if total_steps > 0 else 0)
213
+ st.sidebar.write(f"Progress: {completed}/{total_steps} steps")
214
+ for (idx, _) in steps:
215
+ is_done = st.session_state.app_state['done_flags'].get(idx, False)
216
  label = f"{'βœ“' if is_done else 'Β·'} Step {idx}"
217
  if st.sidebar.button(label, key=f"nav_{idx}"):
218
+ st.session_state.app_state['current_step'] = idx
219
  st.rerun()
220
 
 
221
  def render_tools_list():
222
+ if st.session_state.app_state['tools_list']:
223
+ with st.expander("πŸ”§ Required Tools & Materials", expanded=True):
224
+ for item in st.session_state.app_state['tools_list']:
225
+ st.markdown(f"- {item}")
 
 
226
 
227
  def render_step(idx, text):
228
+ total = len(st.session_state.app_state['steps'])
 
 
 
 
 
 
 
 
 
 
 
229
  st.markdown(f"### Step {idx} of {total}")
230
  st.write(text)
231
+ # Timer, checkbox, notes, and navigation logic remains the same...
232
+ done = st.checkbox("βœ… Mark this step as completed", value=st.session_state.app_state['done_flags'].get(idx, False), key=f"done_{idx}")
233
+ st.session_state.app_state['done_flags'][idx] = done
234
+ notes = st.text_area("πŸ“ Your notes for this step:", value=st.session_state.app_state['notes'].get(idx, ""), height=100, key=f"notes_{idx}")
235
+ st.session_state.app_state['notes'][idx] = notes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  st.markdown("---")
237
  col1, col2, col3 = st.columns([1, 2, 1])
238
+ if idx > 1 and col1.button("⬅️ Previous", key=f"prev_{idx}"):
239
+ st.session_state.app_state['current_step'] -= 1
240
+ st.rerun()
241
+ if idx < total and col3.button("Next ➑️", key=f"next_{idx}"):
242
+ st.session_state.app_state['current_step'] += 1
243
+ st.rerun()
 
 
244
 
245
  # ─────────────────────────────────────────────────────────────────────────────
246
  # 4. APP LAYOUT
 
250
  st.title("πŸ› οΈ NeoFix AI-Powered DIY Assistant")
251
 
252
  with st.expander("ℹ️ How it works", expanded=False):
253
+ st.write("""
254
+ 1. **Upload a photo** of your project.
255
+ 2. **(Optional) Describe your goal** for more accurate results.
256
+ 3. **Review the Plan.** The AI will propose a plan. If you didn't provide a description, you'll be asked to approve it. For crafts, you may get options to choose from.
257
+ 4. **Get Your Guide** with tools and step-by-step instructions.
258
+ 5. **Follow the Steps** using the interactive checklist.
259
+ """)
260
+
261
+ # --- Main UI ---
262
+ if not st.session_state.app_state['prompt_sent']:
263
+ st.markdown("---")
264
+ col1, col2 = st.columns([3, 1])
265
+ with col1:
266
+ uploaded_image = st.file_uploader("πŸ“· Upload a photo of your project", type=["jpg", "jpeg", "png"])
267
+ context_text = st.text_area("✏️ Describe the issue or your goal (optional but recommended)", height=80, placeholder="e.g., 'My toaster won’t turn on,' or 'How do I build a desk like this?'")
268
+ with col2:
269
+ st.markdown("### Actions")
270
+ if st.button("πŸš€ Get AI Guidance", type="primary", use_container_width=True):
271
+ if uploaded_image:
272
+ initial_analysis(uploaded_image, context_text)
273
+ st.rerun()
274
+ else:
275
+ st.warning("⚠️ Please upload an image first!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  if st.button("πŸ”„ Start Over", use_container_width=True):
277
  reset_state()
 
 
278
 
279
+ # --- Results and Steps UI ---
280
+ else:
 
281
  render_sidebar_navigation()
 
 
282
  st.markdown("---")
283
+ st.markdown(f"### {st.session_state.app_state.get('project_title', 'Your Project')}")
284
+ st.markdown(f"**Category:** `{st.session_state.app_state.get('category', 'N/A')}`")
285
+ st.info(f"**Description:** {st.session_state.app_state.get('project_description', 'N/A')}")
 
 
 
 
286
  st.markdown("---")
287
 
288
+ if not st.session_state.app_state['steps']:
289
+ if st.session_state.app_state['upcycling_options']:
290
+ st.markdown("#### The AI has suggested a few projects. Please choose one:")
291
+ for i, option in enumerate(st.session_state.app_state['upcycling_options']):
292
+ if st.button(option, key=f"option_{i}"):
293
+ generate_detailed_steps(selected_option=option)
294
+ st.rerun()
295
+ elif not st.session_state.app_state['plan_approved']:
296
+ st.markdown("#### The AI has proposed the following plan:")
297
+ st.success(st.session_state.app_state['initial_plan'])
298
+ if st.button("βœ… Looks good, proceed with this plan", type="primary"):
299
+ st.session_state.app_state['plan_approved'] = True
300
+ generate_detailed_steps()
301
+ st.rerun()
 
 
 
 
 
 
 
 
302
  else:
303
+ render_tools_list()
304
+ st.markdown("---")
305
+ current_step_index = st.session_state.app_state['current_step']
306
+ try:
307
+ step_num, step_text = st.session_state.app_state['steps'][current_step_index - 1]
308
+ render_step(step_num, step_text)
309
+ except IndexError:
310
+ st.session_state.app_state['current_step'] = 1
311
+ st.rerun()
312
 
313
+ total_steps = len(st.session_state.app_state['steps'])
314
+ done_count = sum(1 for d in st.session_state.app_state['done_flags'].values() if d)
315
+ if total_steps > 0:
316
+ progress = done_count / total_steps
317
+ st.progress(progress)
318
+ st.markdown(f"**Overall Progress:** {done_count} of {total_steps} steps completed ({progress:.0%})")
319
+ if done_count == total_steps:
320
+ st.balloons()
321
+ st.success("πŸŽ‰ Congratulations! You've completed all steps!")
322
+
323
+ if st.button("πŸ”„ Start Over"):
324
+ reset_state()