Marek4321 commited on
Commit
af2bcb1
·
verified ·
1 Parent(s): 4013dd9

Upload 9 files

Browse files
Files changed (9) hide show
  1. .gitignore +167 -0
  2. README.md +165 -0
  3. app.py +219 -0
  4. config.py +66 -0
  5. requirements.txt +6 -0
  6. utils/__init__.py +1 -0
  7. utils/ai_analyzer.py +181 -0
  8. utils/recommendations.py +213 -0
  9. utils/ui_components.py +302 -0
.gitignore ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ # Streamlit
132
+ .streamlit/
133
+
134
+ # API Keys and secrets
135
+ .secrets
136
+ *.key
137
+ api_keys.txt
138
+ config_secrets.py
139
+
140
+ # Images and uploads
141
+ uploads/
142
+ temp_images/
143
+ *.jpg
144
+ *.jpeg
145
+ *.png
146
+ *.gif
147
+ *.bmp
148
+ *.webp
149
+
150
+ # Logs
151
+ *.log
152
+ logs/
153
+
154
+ # IDE
155
+ .vscode/
156
+ .idea/
157
+ *.swp
158
+ *.swo
159
+ *~
160
+
161
+ # OS
162
+ .DS_Store
163
+ Thumbs.db
164
+
165
+ # Temporary files
166
+ *.tmp
167
+ *.temp
README.md ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🏪 Shelf Photo Analyzer
2
+
3
+ ## Description
4
+
5
+ An AI-powered tool for merchandisers and retail managers to instantly analyze product displays on store shelves. Simply upload a shelf photo, enter the product name, and get instant AI analysis with actionable recommendations to improve product placement and boost sales.
6
+
7
+ ## Features
8
+
9
+ - 📸 **Photo Upload** - Support for JPG, PNG, WebP formats up to 10MB
10
+ - 🔍 **Product Search** - AI-powered product detection and analysis
11
+ - 🤖 **Smart Analysis** - Uses GPT-4 Vision API for accurate shelf assessment
12
+ - 📊 **Compliance Scoring** - 1-10 score for product placement effectiveness
13
+ - 💡 **Actionable Recommendations** - Specific suggestions with impact estimates
14
+ - 📱 **Mobile Friendly** - Responsive design for field use
15
+ - 📈 **Impact Forecasting** - Estimated sales improvement from changes
16
+
17
+ ## How to Use
18
+
19
+ 1. **Upload** a clear photo of the store shelf
20
+ 2. **Enter** the product name you want to analyze
21
+ 3. **Select** analysis depth (Basic/Detailed/Expert)
22
+ 4. **Click** "Analyze Photo" and wait 15-30 seconds
23
+ 5. **Review** results and follow the recommendations
24
+
25
+ ## Analysis Results
26
+
27
+ The AI provides comprehensive analysis including:
28
+
29
+ - ✅ **Product Detection** - Whether product is visible on shelf
30
+ - 📦 **Facing Count** - Number of visible product units
31
+ - 📍 **Shelf Position** - Top/middle/bottom placement
32
+ - 💰 **Price Visibility** - Whether pricing is clearly shown
33
+ - 🧹 **Product Condition** - Cleanliness and damage assessment
34
+ - 🏆 **Overall Score** - 1-10 rating for placement effectiveness
35
+ - 🎯 **Confidence Level** - AI certainty in the analysis
36
+
37
+ ## Recommendations Engine
38
+
39
+ Get prioritized action items with:
40
+
41
+ - 🚨 **Priority Levels** - Critical, high, medium, low priorities
42
+ - ⏱️ **Time Estimates** - How long each fix will take
43
+ - 📈 **Impact Predictions** - Expected sales/visibility improvements
44
+ - 🔧 **Difficulty Rating** - How easy each recommendation is to implement
45
+
46
+ ## Tech Stack
47
+
48
+ - **Frontend:** Streamlit
49
+ - **AI:** OpenAI GPT-4 Vision API
50
+ - **Image Processing:** Pillow (PIL)
51
+ - **Backend:** Python 3.9+
52
+ - **Hosting:** Hugging Face Spaces
53
+
54
+ ## Setup Instructions
55
+
56
+ ### For Hugging Face Spaces Deployment
57
+
58
+ 1. **Fork/Clone** this repository
59
+ 2. **Create** a new Hugging Face Space
60
+ 3. **Select** "Streamlit" as the SDK
61
+ 4. **Upload** all files to your Space
62
+ 5. **Add** your OpenAI API key to Space secrets:
63
+ - Go to Settings → Repository secrets
64
+ - Add: `OPENAI_API_KEY = "your-api-key-here"`
65
+ 6. **Deploy** - your app will be live in minutes!
66
+
67
+ ### Local Development
68
+
69
+ ```bash
70
+ # Clone repository
71
+ git clone <repository-url>
72
+ cd shelf-analyzer
73
+
74
+ # Install dependencies
75
+ pip install -r requirements.txt
76
+
77
+ # Set environment variable
78
+ export OPENAI_API_KEY="your-api-key-here"
79
+
80
+ # Run application
81
+ streamlit run app.py
82
+ ```
83
+
84
+ ## Configuration
85
+
86
+ The app uses the following configuration (in `config.py`):
87
+
88
+ - **Max Image Size:** 10MB
89
+ - **Supported Formats:** JPG, JPEG, PNG, WebP
90
+ - **AI Model:** GPT-4 Vision Preview
91
+ - **Analysis Timeout:** 60 seconds
92
+ - **Max Retries:** 3 attempts
93
+
94
+ ## File Structure
95
+
96
+ ```
97
+ shelf-analyzer/
98
+ ├── app.py # Main Streamlit application
99
+ ├── requirements.txt # Python dependencies
100
+ ├── config.py # Configuration settings
101
+ ├── README.md # This file
102
+ ├── utils/
103
+ │ ├── __init__.py
104
+ │ ├── ai_analyzer.py # AI analysis logic
105
+ │ ├── recommendations.py # Recommendation engine
106
+ │ └── ui_components.py # UI display components
107
+ ```
108
+
109
+ ## Usage Examples
110
+
111
+ ### Typical Use Cases
112
+
113
+ - **Merchandisers:** Check product compliance during store visits
114
+ - **Category Managers:** Audit shelf execution across locations
115
+ - **Store Managers:** Optimize product placement for better sales
116
+ - **Retail Auditors:** Document and track display improvements
117
+
118
+ ### Sample Workflow
119
+
120
+ 1. Walk store aisles with smartphone/tablet
121
+ 2. Take photos of key product categories
122
+ 3. Analyze each product's shelf presence
123
+ 4. Follow AI recommendations immediately
124
+ 5. Re-analyze to confirm improvements
125
+
126
+ ## API Costs
127
+
128
+ - **Hugging Face Hosting:** FREE ✨
129
+ - **OpenAI API:** ~$0.01-0.04 per analysis
130
+ - **Monthly Estimate:** $20-100 (depending on usage)
131
+
132
+ ## Limitations
133
+
134
+ - Requires good lighting in photos
135
+ - Works best with clear, unobstructed shelf views
136
+ - AI accuracy depends on image quality
137
+ - Currently supports single product analysis per photo
138
+
139
+ ## Future Roadmap
140
+
141
+ ### Phase 2
142
+ - [ ] Batch analysis of multiple products
143
+ - [ ] PDF report generation
144
+ - [ ] Analysis history dashboard
145
+ - [ ] Popular product presets
146
+
147
+ ### Phase 3
148
+ - [ ] Mobile app development
149
+ - [ ] Real-time video analysis
150
+ - [ ] Planogram compliance checking
151
+ - [ ] Team collaboration features
152
+
153
+ ## Support
154
+
155
+ For issues, questions, or feature requests:
156
+ - Create an issue in this repository
157
+ - Contact: [your-contact-info]
158
+
159
+ ## License
160
+
161
+ This project is licensed under the MIT License - see the LICENSE file for details.
162
+
163
+ ---
164
+
165
+ **Powered by OpenAI GPT-4 Vision API**
app.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ from PIL import Image
4
+ import base64
5
+ from io import BytesIO
6
+ from utils.ai_analyzer import analyze_shelf_image
7
+ from utils.recommendations import generate_recommendations
8
+ from utils.ui_components import display_results, display_recommendations
9
+
10
+ st.set_page_config(
11
+ page_title="Shelf Photo Analyzer",
12
+ page_icon="🏪",
13
+ layout="wide",
14
+ initial_sidebar_state="collapsed"
15
+ )
16
+
17
+ st.markdown("""
18
+ <style>
19
+ .main-header {
20
+ font-size: 3rem;
21
+ color: #FF6B6B;
22
+ text-align: center;
23
+ margin-bottom: 1rem;
24
+ }
25
+ .subtitle {
26
+ font-size: 1.2rem;
27
+ text-align: center;
28
+ color: #666;
29
+ margin-bottom: 2rem;
30
+ }
31
+ .upload-section {
32
+ border: 2px dashed #FF6B6B;
33
+ border-radius: 10px;
34
+ padding: 2rem;
35
+ text-align: center;
36
+ margin: 1rem 0;
37
+ }
38
+ .results-container {
39
+ background-color: #f8f9fa;
40
+ border-radius: 10px;
41
+ padding: 1.5rem;
42
+ margin: 1rem 0;
43
+ }
44
+ </style>
45
+ """, unsafe_allow_html=True)
46
+
47
+ def main():
48
+ st.markdown('<h1 class="main-header">🏪 Shelf Photo Analyzer</h1>', unsafe_allow_html=True)
49
+ st.markdown('<p class="subtitle">Błyskawiczna analiza ekspozycji produktów na półkach sklepowych przy użyciu AI</p>', unsafe_allow_html=True)
50
+
51
+ # Initialize session state
52
+ if 'analysis_results' not in st.session_state:
53
+ st.session_state.analysis_results = None
54
+ if 'recommendations' not in st.session_state:
55
+ st.session_state.recommendations = None
56
+ if 'analysis_history' not in st.session_state:
57
+ st.session_state.analysis_history = []
58
+ if 'openai_api_key' not in st.session_state:
59
+ st.session_state.openai_api_key = ''
60
+
61
+ # API Key input
62
+ st.markdown("### 🔑 OpenAI API Configuration")
63
+ api_key_input = st.text_input(
64
+ "Enter your OpenAI API Key",
65
+ type="password",
66
+ value=st.session_state.openai_api_key,
67
+ placeholder="sk-...",
68
+ help="Your API key is stored only for this session and never saved permanently"
69
+ )
70
+
71
+ if api_key_input != st.session_state.openai_api_key:
72
+ st.session_state.openai_api_key = api_key_input
73
+
74
+ if not st.session_state.openai_api_key:
75
+ st.warning("⚠️ Please enter your OpenAI API key to use the analyzer. You can get one at: https://platform.openai.com/api-keys")
76
+ st.info("💡 **Tip:** Your API key is only stored temporarily in your browser session and is never saved permanently.")
77
+ return
78
+
79
+ st.success("✅ API key configured successfully!")
80
+ st.markdown("---")
81
+
82
+ # Main interface
83
+ col1, col2 = st.columns([1, 1])
84
+
85
+ with col1:
86
+ st.markdown("### 📸 Upload zdjęcia półki")
87
+ uploaded_file = st.file_uploader(
88
+ "Wybierz zdjęcie półki sklepowej",
89
+ type=['jpg', 'jpeg', 'png', 'webp'],
90
+ help="Obsługiwane formaty: JPG, PNG, WebP (max 10MB)"
91
+ )
92
+
93
+ if uploaded_file is not None:
94
+ try:
95
+ image = Image.open(uploaded_file)
96
+ st.image(image, caption="Uploaded Image", use_column_width=True)
97
+ st.session_state.uploaded_image = image
98
+ except Exception as e:
99
+ st.error(f"Błąd przy wczytywaniu obrazu: {str(e)}")
100
+
101
+ with col2:
102
+ st.markdown("### 🔍 Nazwa produktu")
103
+ product_name = st.text_input(
104
+ "Wpisz nazwę produktu do wyszukania",
105
+ placeholder="np. Coca-Cola 0.5L, Milka czekolada...",
106
+ help="Wpisz konkretną nazwę produktu, który chcesz znaleźć na półce"
107
+ )
108
+
109
+ st.markdown("### ⚙️ Opcje analizy")
110
+ analysis_depth = st.selectbox(
111
+ "Poziom szczegółowości",
112
+ ["Podstawowy", "Szczegółowy", "Ekspercki"],
113
+ help="Wybierz poziom analizy - im wyższy, tym więcej szczegółów"
114
+ )
115
+
116
+ # Analysis button
117
+ if st.button("🚀 Analizuj zdjęcie", type="primary", use_container_width=True):
118
+ if not st.session_state.openai_api_key:
119
+ st.error("⚠️ Proszę najpierw wprowadzić klucz API!")
120
+ elif uploaded_file is None:
121
+ st.error("⚠️ Proszę najpierw wgrać zdjęcie!")
122
+ elif not product_name.strip():
123
+ st.error("⚠️ Proszę wpisać nazwę produktu!")
124
+ else:
125
+ with st.spinner("🤖 Analizuję zdjęcie... To może potrwać do 30 sekund"):
126
+ try:
127
+ # Convert image to base64 for API
128
+ buffered = BytesIO()
129
+ image.save(buffered, format="JPEG")
130
+ image_base64 = base64.b64encode(buffered.getvalue()).decode()
131
+
132
+ # Analyze image
133
+ analysis_results = analyze_shelf_image(
134
+ image_base64,
135
+ product_name,
136
+ analysis_depth
137
+ )
138
+
139
+ if analysis_results:
140
+ st.session_state.analysis_results = analysis_results
141
+ st.session_state.recommendations = generate_recommendations(analysis_results)
142
+
143
+ # Add to history
144
+ st.session_state.analysis_history.append({
145
+ 'product_name': product_name,
146
+ 'analysis_date': st.session_state.get('current_time', 'Unknown'),
147
+ 'results': analysis_results
148
+ })
149
+
150
+ st.success("✅ Analiza zakończona pomyślnie!")
151
+ st.rerun()
152
+ else:
153
+ st.error("❌ Wystąpił błąd podczas analizy. Spróbuj ponownie.")
154
+
155
+ except Exception as e:
156
+ st.error(f"❌ Błąd podczas analizy: {str(e)}")
157
+
158
+ # Display results
159
+ if st.session_state.analysis_results:
160
+ st.markdown("---")
161
+ st.markdown("## 📊 Wyniki analizy")
162
+
163
+ # Display analysis results
164
+ display_results(st.session_state.analysis_results, product_name)
165
+
166
+ # Display recommendations
167
+ if st.session_state.recommendations:
168
+ st.markdown("## 💡 Rekomendacje")
169
+ display_recommendations(st.session_state.recommendations)
170
+
171
+ # Export options
172
+ col1, col2, col3 = st.columns(3)
173
+ with col1:
174
+ if st.button("📄 Eksportuj do PDF"):
175
+ st.info("🚧 Funkcja w przygotowaniu")
176
+ with col2:
177
+ if st.button("📧 Wyślij email"):
178
+ st.info("🚧 Funkcja w przygotowaniu")
179
+ with col3:
180
+ if st.button("🔄 Nowa analiza"):
181
+ st.session_state.analysis_results = None
182
+ st.session_state.recommendations = None
183
+ st.rerun()
184
+
185
+ # Sidebar with history and info
186
+ with st.sidebar:
187
+ st.markdown("## 📈 Historia analiz")
188
+ if st.session_state.analysis_history:
189
+ for i, analysis in enumerate(reversed(st.session_state.analysis_history[-5:])):
190
+ with st.expander(f"{analysis['product_name']} ({analysis['analysis_date']})"):
191
+ score = analysis['results'].get('overall_score', 'N/A')
192
+ st.write(f"Ocena: {score}/10")
193
+ if analysis['results'].get('product_found'):
194
+ st.write("✅ Produkt znaleziony")
195
+ else:
196
+ st.write("❌ Produkt nie znaleziony")
197
+ else:
198
+ st.write("Brak historii analiz")
199
+
200
+ st.markdown("---")
201
+ st.markdown("## ℹ️ Informacje")
202
+ st.markdown("""
203
+ **Jak używać:**
204
+ 1. Wgraj zdjęcie półki
205
+ 2. Wpisz nazwę produktu
206
+ 3. Kliknij "Analizuj"
207
+ 4. Otrzymaj rekomendacje
208
+
209
+ **Wskazówki:**
210
+ - Rób zdjęcia z dobrem oświetleniu
211
+ - Upewnij się, że produkty są widoczne
212
+ - Używaj konkretnych nazw produktów
213
+ """)
214
+
215
+ st.markdown("---")
216
+ st.markdown("**Powered by OpenAI GPT-4 Vision**")
217
+
218
+ if __name__ == "__main__":
219
+ main()
config.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+
4
+ # OpenAI Configuration
5
+ def get_openai_api_key():
6
+ """Get OpenAI API key from user input in session state"""
7
+ api_key = st.session_state.get('openai_api_key', '')
8
+ if not api_key:
9
+ return None
10
+ return api_key
11
+
12
+ # Application Configuration
13
+ MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10MB
14
+ SUPPORTED_FORMATS = ['jpg', 'jpeg', 'png', 'webp']
15
+ AI_MODEL = "gpt-4-vision-preview"
16
+
17
+ # Analysis Settings
18
+ ANALYSIS_TIMEOUT = 60 # seconds
19
+ MAX_RETRY_ATTEMPTS = 3
20
+
21
+ # UI Configuration
22
+ PRIMARY_COLOR = "#FF6B6B"
23
+ BACKGROUND_COLOR = "#FFFFFF"
24
+ SECONDARY_BACKGROUND_COLOR = "#F0F2F6"
25
+ TEXT_COLOR = "#262730"
26
+
27
+ # Prompt Templates
28
+ ANALYSIS_PROMPT_TEMPLATE = """
29
+ Jesteś ekspertem w analizie ekspozycji produktów w sklepach detalicznych.
30
+ Przeanalizuj to zdjęcie półki pod kątem obecności produktu: "{product_name}".
31
+
32
+ Zwróć wyniki w formacie JSON z następującymi polami:
33
+ {{
34
+ "product_found": true/false,
35
+ "facing_count": liczba_widocznych_sztuk,
36
+ "shelf_position": "top"/"middle"/"bottom",
37
+ "price_visible": true/false,
38
+ "product_condition": "good"/"dusty"/"damaged",
39
+ "overall_score": ocena_1_10,
40
+ "confidence": pewność_0_1,
41
+ "description": "szczegółowy_opis_sytuacji",
42
+ "competitors_nearby": ["lista_konkurencyjnych_produktów"],
43
+ "shelf_share": procent_zajętości_półki
44
+ }}
45
+
46
+ Poziom analizy: {analysis_depth}
47
+ """
48
+
49
+ RECOMMENDATIONS_RULES = {
50
+ "position": {
51
+ "eye_level": "Przenieś produkty na poziom oczu (zwiększa sprzedaż o 30%)",
52
+ "visibility": "Popraw widoczność produktów"
53
+ },
54
+ "quantity": {
55
+ "increase_facings": "Dodaj dodatkowe facings",
56
+ "restock": "Uzupełnij brakujące produkty"
57
+ },
58
+ "condition": {
59
+ "cleaning": "Wyczyść produkty i opakowania",
60
+ "replace_damaged": "Wymień uszkodzone produkty"
61
+ },
62
+ "pricing": {
63
+ "add_price": "Dodaj widoczną cenę",
64
+ "update_price": "Zaktualizuj cenę"
65
+ }
66
+ }
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit>=1.28.0
2
+ Pillow>=10.0.0
3
+ openai>=1.3.0
4
+ pandas>=2.0.0
5
+ numpy>=1.24.0
6
+ requests>=2.31.0
utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Utils package for Shelf Photo Analyzer
utils/ai_analyzer.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import openai
2
+ import streamlit as st
3
+ import json
4
+ from typing import Dict, Optional
5
+ from config import get_openai_api_key, ANALYSIS_PROMPT_TEMPLATE, AI_MODEL
6
+
7
+ def analyze_shelf_image(image_base64: str, product_name: str, analysis_depth: str = "Podstawowy") -> Optional[Dict]:
8
+ """
9
+ Analyze shelf image using OpenAI GPT-4 Vision API
10
+
11
+ Args:
12
+ image_base64: Base64 encoded image
13
+ product_name: Name of product to search for
14
+ analysis_depth: Level of analysis detail
15
+
16
+ Returns:
17
+ Dictionary with analysis results or None if failed
18
+ """
19
+ try:
20
+ # Get API key and validate
21
+ api_key = get_openai_api_key()
22
+ if not api_key:
23
+ st.error("⚠️ OpenAI API key not configured!")
24
+ return None
25
+
26
+ # Initialize OpenAI client
27
+ client = openai.OpenAI(api_key=api_key)
28
+
29
+ # Prepare the prompt
30
+ prompt = ANALYSIS_PROMPT_TEMPLATE.format(
31
+ product_name=product_name,
32
+ analysis_depth=analysis_depth
33
+ )
34
+
35
+ # Make API call
36
+ response = client.chat.completions.create(
37
+ model=AI_MODEL,
38
+ messages=[
39
+ {
40
+ "role": "user",
41
+ "content": [
42
+ {
43
+ "type": "text",
44
+ "text": prompt
45
+ },
46
+ {
47
+ "type": "image_url",
48
+ "image_url": {
49
+ "url": f"data:image/jpeg;base64,{image_base64}",
50
+ "detail": "high"
51
+ }
52
+ }
53
+ ]
54
+ }
55
+ ],
56
+ max_tokens=1500,
57
+ temperature=0.1
58
+ )
59
+
60
+ # Parse response
61
+ content = response.choices[0].message.content
62
+
63
+ # Try to extract JSON from response
64
+ try:
65
+ # Find JSON in the response
66
+ start_idx = content.find('{')
67
+ end_idx = content.rfind('}') + 1
68
+
69
+ if start_idx != -1 and end_idx != -1:
70
+ json_str = content[start_idx:end_idx]
71
+ analysis_results = json.loads(json_str)
72
+
73
+ # Validate required fields
74
+ required_fields = ['product_found', 'overall_score', 'confidence']
75
+ for field in required_fields:
76
+ if field not in analysis_results:
77
+ st.warning(f"Missing required field: {field}")
78
+ analysis_results[field] = get_default_value(field)
79
+
80
+ # Ensure proper data types
81
+ analysis_results = normalize_analysis_results(analysis_results)
82
+
83
+ return analysis_results
84
+ else:
85
+ st.error("Could not find JSON in AI response")
86
+ return create_fallback_result(product_name, content)
87
+
88
+ except json.JSONDecodeError as e:
89
+ st.error(f"Failed to parse AI response as JSON: {str(e)}")
90
+ return create_fallback_result(product_name, content)
91
+
92
+ except Exception as e:
93
+ st.error(f"Error during AI analysis: {str(e)}")
94
+ return None
95
+
96
+ def normalize_analysis_results(results: Dict) -> Dict:
97
+ """Normalize analysis results to ensure proper data types"""
98
+ try:
99
+ # Ensure boolean fields
100
+ bool_fields = ['product_found', 'price_visible']
101
+ for field in bool_fields:
102
+ if field in results:
103
+ if isinstance(results[field], str):
104
+ results[field] = results[field].lower() in ['true', '1', 'yes', 'tak']
105
+ else:
106
+ results[field] = bool(results[field])
107
+
108
+ # Ensure numeric fields
109
+ if 'overall_score' in results:
110
+ try:
111
+ results['overall_score'] = max(1, min(10, int(float(results['overall_score']))))
112
+ except:
113
+ results['overall_score'] = 5
114
+
115
+ if 'confidence' in results:
116
+ try:
117
+ results['confidence'] = max(0.0, min(1.0, float(results['confidence'])))
118
+ except:
119
+ results['confidence'] = 0.5
120
+
121
+ if 'facing_count' in results:
122
+ try:
123
+ results['facing_count'] = max(0, int(results['facing_count']))
124
+ except:
125
+ results['facing_count'] = 0
126
+
127
+ if 'shelf_share' in results:
128
+ try:
129
+ results['shelf_share'] = max(0, min(100, float(results['shelf_share'])))
130
+ except:
131
+ results['shelf_share'] = 0
132
+
133
+ # Ensure string fields
134
+ string_fields = ['shelf_position', 'product_condition', 'description']
135
+ for field in string_fields:
136
+ if field in results and not isinstance(results[field], str):
137
+ results[field] = str(results[field])
138
+
139
+ # Ensure list fields
140
+ if 'competitors_nearby' in results and not isinstance(results['competitors_nearby'], list):
141
+ results['competitors_nearby'] = []
142
+
143
+ return results
144
+
145
+ except Exception as e:
146
+ st.warning(f"Error normalizing results: {str(e)}")
147
+ return results
148
+
149
+ def get_default_value(field: str):
150
+ """Get default value for missing field"""
151
+ defaults = {
152
+ 'product_found': False,
153
+ 'facing_count': 0,
154
+ 'shelf_position': 'unknown',
155
+ 'price_visible': False,
156
+ 'product_condition': 'unknown',
157
+ 'overall_score': 5,
158
+ 'confidence': 0.5,
159
+ 'description': 'Analysis incomplete',
160
+ 'competitors_nearby': [],
161
+ 'shelf_share': 0
162
+ }
163
+ return defaults.get(field, None)
164
+
165
+ def create_fallback_result(product_name: str, ai_response: str) -> Dict:
166
+ """Create fallback result when JSON parsing fails"""
167
+ # Try to extract basic information from text response
168
+ product_found = any(word in ai_response.lower() for word in ['found', 'visible', 'present', 'znaleziono', 'widoczny'])
169
+
170
+ return {
171
+ 'product_found': product_found,
172
+ 'facing_count': 1 if product_found else 0,
173
+ 'shelf_position': 'unknown',
174
+ 'price_visible': 'cena' in ai_response.lower() or 'price' in ai_response.lower(),
175
+ 'product_condition': 'good',
176
+ 'overall_score': 6 if product_found else 3,
177
+ 'confidence': 0.6,
178
+ 'description': f"Analysis of {product_name}: {ai_response[:200]}...",
179
+ 'competitors_nearby': [],
180
+ 'shelf_share': 10 if product_found else 0
181
+ }
utils/recommendations.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List
2
+ from config import RECOMMENDATIONS_RULES
3
+
4
+ def generate_recommendations(analysis_results: Dict) -> List[Dict]:
5
+ """
6
+ Generate actionable recommendations based on analysis results
7
+
8
+ Args:
9
+ analysis_results: Dictionary with analysis results from AI
10
+
11
+ Returns:
12
+ List of recommendation dictionaries
13
+ """
14
+ recommendations = []
15
+
16
+ if not analysis_results:
17
+ return recommendations
18
+
19
+ # Product not found recommendations
20
+ if not analysis_results.get('product_found', False):
21
+ recommendations.append({
22
+ 'type': 'critical',
23
+ 'priority': 1,
24
+ 'title': '🚨 Produkt nie znaleziony',
25
+ 'description': 'Produkt nie jest widoczny na półce lub jest niedostępny.',
26
+ 'action': 'Uzupełnij asortyment lub sprawdź lokalizację produktu',
27
+ 'estimated_impact': '+100% dostępności',
28
+ 'time_to_fix': '5-15 minut',
29
+ 'difficulty': 'Łatwe'
30
+ })
31
+ return recommendations
32
+
33
+ # Position recommendations
34
+ shelf_position = analysis_results.get('shelf_position', '').lower()
35
+ if shelf_position in ['bottom', 'dół', 'dolna']:
36
+ recommendations.append({
37
+ 'type': 'position',
38
+ 'priority': 2,
39
+ 'title': '📈 Przenieś na poziom oczu',
40
+ 'description': 'Produkt znajduje się na dolnej półce, co znacznie zmniejsza widoczność.',
41
+ 'action': 'Przenieś produkty na poziom oczu (120-160cm)',
42
+ 'estimated_impact': '+30% sprzedaży',
43
+ 'time_to_fix': '2-3 minuty',
44
+ 'difficulty': 'Łatwe'
45
+ })
46
+ elif shelf_position in ['top', 'góra', 'górna']:
47
+ recommendations.append({
48
+ 'type': 'position',
49
+ 'priority': 3,
50
+ 'title': '👁️ Popraw widoczność',
51
+ 'description': 'Produkt na górnej półce - rozważ przeniesienie lub dodatkowe oznakowanie.',
52
+ 'action': 'Przenieś na środkową półkę lub dodaj shelf talker',
53
+ 'estimated_impact': '+20% widoczności',
54
+ 'time_to_fix': '2-3 minuty',
55
+ 'difficulty': 'Łatwe'
56
+ })
57
+
58
+ # Facing count recommendations
59
+ facing_count = analysis_results.get('facing_count', 0)
60
+ if facing_count == 1:
61
+ recommendations.append({
62
+ 'type': 'quantity',
63
+ 'priority': 3,
64
+ 'title': '➕ Zwiększ liczbę facings',
65
+ 'description': 'Tylko jeden facing - zwiększenie do 2-3 poprawia widoczność.',
66
+ 'action': 'Dodaj 1-2 dodatkowe facings',
67
+ 'estimated_impact': '+15% widoczności',
68
+ 'time_to_fix': '1-2 minuty',
69
+ 'difficulty': 'Bardzo łatwe'
70
+ })
71
+ elif facing_count == 0:
72
+ recommendations.append({
73
+ 'type': 'quantity',
74
+ 'priority': 1,
75
+ 'title': '🔄 Uzupełnij produkty',
76
+ 'description': 'Brak produktów na półce.',
77
+ 'action': 'Uzupełnij asortyment z magazynu',
78
+ 'estimated_impact': '+100% dostępności',
79
+ 'time_to_fix': '5-10 minut',
80
+ 'difficulty': 'Łatwe'
81
+ })
82
+
83
+ # Price visibility recommendations
84
+ if not analysis_results.get('price_visible', False):
85
+ recommendations.append({
86
+ 'type': 'pricing',
87
+ 'priority': 2,
88
+ 'title': '💰 Dodaj widoczną cenę',
89
+ 'description': 'Brak widocznej ceny może zniechęcać klientów.',
90
+ 'action': 'Dodaj lub popraw czytelność cenówki',
91
+ 'estimated_impact': '+10% konwersji',
92
+ 'time_to_fix': '1-2 minuty',
93
+ 'difficulty': 'Bardzo łatwe'
94
+ })
95
+
96
+ # Product condition recommendations
97
+ condition = analysis_results.get('product_condition', '').lower()
98
+ if condition in ['dusty', 'zakurzony']:
99
+ recommendations.append({
100
+ 'type': 'condition',
101
+ 'priority': 4,
102
+ 'title': '🧹 Wyczyść produkty',
103
+ 'description': 'Produkty wyglądają na zakurzone, co wpływa na postrzeganie jakości.',
104
+ 'action': 'Wyczyść produkty i opakowania',
105
+ 'estimated_impact': '+5% percepcji jakości',
106
+ 'time_to_fix': '2-3 minuty',
107
+ 'difficulty': 'Bardzo łatwe'
108
+ })
109
+ elif condition in ['damaged', 'uszkodzony']:
110
+ recommendations.append({
111
+ 'type': 'condition',
112
+ 'priority': 1,
113
+ 'title': '⚠️ Wymień uszkodzone produkty',
114
+ 'description': 'Uszkodzone produkty negatywnie wpływają na wizerunek marki.',
115
+ 'action': 'Usuń uszkodzone produkty i zastąp nowymi',
116
+ 'estimated_impact': '+15% percepcji marki',
117
+ 'time_to_fix': '3-5 minut',
118
+ 'difficulty': 'Łatwe'
119
+ })
120
+
121
+ # Competition recommendations
122
+ competitors = analysis_results.get('competitors_nearby', [])
123
+ if len(competitors) > 3:
124
+ recommendations.append({
125
+ 'type': 'competition',
126
+ 'priority': 3,
127
+ 'title': '🎯 Zwiększ wyróżnienie',
128
+ 'description': f'Silna konkurencja w pobliżu ({len(competitors)} produktów konkurencyjnych).',
129
+ 'action': 'Dodaj materiały POS lub zwiększ liczbę facings',
130
+ 'estimated_impact': '+20% uwagi klientów',
131
+ 'time_to_fix': '5-10 minut',
132
+ 'difficulty': 'Średnie'
133
+ })
134
+
135
+ # Shelf share recommendations
136
+ shelf_share = analysis_results.get('shelf_share', 0)
137
+ if shelf_share < 10:
138
+ recommendations.append({
139
+ 'type': 'share',
140
+ 'priority': 2,
141
+ 'title': '📊 Zwiększ udział w półce',
142
+ 'description': f'Niski udział w półce ({shelf_share}%). Rozważ negocjacje z menedżerem kategorii.',
143
+ 'action': 'Zwiększ liczbę facings lub wynegocjuj lepszą przestrzeń',
144
+ 'estimated_impact': '+25% widoczności',
145
+ 'time_to_fix': '10-30 minut',
146
+ 'difficulty': 'Trudne'
147
+ })
148
+
149
+ # Overall score recommendations
150
+ overall_score = analysis_results.get('overall_score', 5)
151
+ if overall_score < 4:
152
+ recommendations.append({
153
+ 'type': 'general',
154
+ 'priority': 1,
155
+ 'title': '🚀 Kompleksowa poprawa',
156
+ 'description': 'Niska ogólna ocena ekspozycji wymaga kompleksowej poprawy.',
157
+ 'action': 'Zastosuj wszystkie powyższe rekomendacje w kolejności priorytetów',
158
+ 'estimated_impact': '+50% ogólnej efektywności',
159
+ 'time_to_fix': '15-30 minut',
160
+ 'difficulty': 'Średnie'
161
+ })
162
+
163
+ # Sort recommendations by priority
164
+ recommendations.sort(key=lambda x: x['priority'])
165
+
166
+ return recommendations
167
+
168
+ def calculate_total_impact(recommendations: List[Dict]) -> Dict:
169
+ """
170
+ Calculate estimated total impact of all recommendations
171
+
172
+ Args:
173
+ recommendations: List of recommendation dictionaries
174
+
175
+ Returns:
176
+ Dictionary with total impact estimates
177
+ """
178
+ total_time = 0
179
+ impact_categories = {
180
+ 'sprzedaż': [],
181
+ 'widoczność': [],
182
+ 'dostępność': [],
183
+ 'jakość': []
184
+ }
185
+
186
+ for rec in recommendations:
187
+ # Extract time (assuming format like "2-3 minuty")
188
+ time_str = rec.get('time_to_fix', '0')
189
+ try:
190
+ # Extract first number from time string
191
+ time_parts = time_str.split('-')
192
+ if time_parts:
193
+ total_time += int(''.join(filter(str.isdigit, time_parts[0])))
194
+ except:
195
+ total_time += 5 # Default 5 minutes
196
+
197
+ # Categorize impact
198
+ impact = rec.get('estimated_impact', '')
199
+ if 'sprzedaż' in impact.lower():
200
+ impact_categories['sprzedaż'].append(impact)
201
+ elif 'widoczność' in impact.lower():
202
+ impact_categories['widoczność'].append(impact)
203
+ elif 'dostępność' in impact.lower():
204
+ impact_categories['dostępność'].append(impact)
205
+ elif any(word in impact.lower() for word in ['jakość', 'percepcj']):
206
+ impact_categories['jakość'].append(impact)
207
+
208
+ return {
209
+ 'total_time_minutes': total_time,
210
+ 'impact_categories': impact_categories,
211
+ 'total_recommendations': len(recommendations),
212
+ 'high_priority_count': len([r for r in recommendations if r['priority'] <= 2])
213
+ }
utils/ui_components.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from typing import Dict, List
3
+ from utils.recommendations import calculate_total_impact
4
+
5
+ def display_results(analysis_results: Dict, product_name: str):
6
+ """Display analysis results in a formatted way"""
7
+
8
+ col1, col2, col3 = st.columns(3)
9
+
10
+ with col1:
11
+ # Overall score
12
+ score = analysis_results.get('overall_score', 5)
13
+ if score >= 8:
14
+ score_color = "🟢"
15
+ score_text = "Excellent"
16
+ elif score >= 6:
17
+ score_color = "🟡"
18
+ score_text = "Good"
19
+ else:
20
+ score_color = "🔴"
21
+ score_text = "Needs Improvement"
22
+
23
+ st.metric(
24
+ label="Overall Score",
25
+ value=f"{score}/10",
26
+ delta=score_text,
27
+ help="Overall assessment of product placement"
28
+ )
29
+
30
+ with col2:
31
+ # Confidence
32
+ confidence = analysis_results.get('confidence', 0.5)
33
+ confidence_pct = int(confidence * 100)
34
+ st.metric(
35
+ label="AI Confidence",
36
+ value=f"{confidence_pct}%",
37
+ help="How confident the AI is in this analysis"
38
+ )
39
+
40
+ with col3:
41
+ # Product found status
42
+ found = analysis_results.get('product_found', False)
43
+ status = "✅ Found" if found else "❌ Not Found"
44
+ st.metric(
45
+ label="Product Status",
46
+ value=status,
47
+ help="Whether the product was detected on the shelf"
48
+ )
49
+
50
+ st.markdown("---")
51
+
52
+ # Detailed information
53
+ col1, col2 = st.columns(2)
54
+
55
+ with col1:
56
+ st.markdown("### 📊 Product Details")
57
+
58
+ # Facing count
59
+ facing_count = analysis_results.get('facing_count', 0)
60
+ st.info(f"**Facings:** {facing_count} visible units")
61
+
62
+ # Shelf position
63
+ position = analysis_results.get('shelf_position', 'Unknown')
64
+ position_emoji = {
65
+ 'top': '⬆️',
66
+ 'middle': '➡️',
67
+ 'bottom': '⬇️',
68
+ 'górna': '⬆️',
69
+ 'środkowa': '➡️',
70
+ 'dolna': '⬇️'
71
+ }.get(position.lower(), '❓')
72
+ st.info(f"**Position:** {position_emoji} {position.title()}")
73
+
74
+ # Price visibility
75
+ price_visible = analysis_results.get('price_visible', False)
76
+ price_status = "✅ Visible" if price_visible else "❌ Not visible"
77
+ st.info(f"**Price:** {price_status}")
78
+
79
+ with col2:
80
+ st.markdown("### 🔍 Quality Assessment")
81
+
82
+ # Product condition
83
+ condition = analysis_results.get('product_condition', 'Unknown')
84
+ condition_emoji = {
85
+ 'good': '✅',
86
+ 'dusty': '🧹',
87
+ 'damaged': '⚠️',
88
+ 'dobry': '✅',
89
+ 'zakurzony': '🧹',
90
+ 'uszkodzony': '⚠️'
91
+ }.get(condition.lower(), '❓')
92
+ st.info(f"**Condition:** {condition_emoji} {condition.title()}")
93
+
94
+ # Shelf share
95
+ shelf_share = analysis_results.get('shelf_share', 0)
96
+ if shelf_share > 0:
97
+ st.info(f"**Shelf Share:** {shelf_share}% of shelf space")
98
+
99
+ # Competitors
100
+ competitors = analysis_results.get('competitors_nearby', [])
101
+ if competitors:
102
+ st.info(f"**Nearby Competitors:** {len(competitors)} products")
103
+
104
+ # Description
105
+ description = analysis_results.get('description', '')
106
+ if description:
107
+ st.markdown("### 📝 Analysis Details")
108
+ st.markdown(f"> {description}")
109
+
110
+ def display_recommendations(recommendations: List[Dict]):
111
+ """Display recommendations in a formatted way"""
112
+
113
+ if not recommendations:
114
+ st.info("No specific recommendations generated.")
115
+ return
116
+
117
+ # Calculate total impact
118
+ impact_summary = calculate_total_impact(recommendations)
119
+
120
+ # Display summary metrics
121
+ col1, col2, col3, col4 = st.columns(4)
122
+
123
+ with col1:
124
+ st.metric(
125
+ "Total Recommendations",
126
+ impact_summary['total_recommendations'],
127
+ help="Number of actionable recommendations"
128
+ )
129
+
130
+ with col2:
131
+ st.metric(
132
+ "High Priority",
133
+ impact_summary['high_priority_count'],
134
+ help="Critical issues requiring immediate attention"
135
+ )
136
+
137
+ with col3:
138
+ st.metric(
139
+ "Estimated Time",
140
+ f"{impact_summary['total_time_minutes']} min",
141
+ help="Total time needed to implement all changes"
142
+ )
143
+
144
+ with col4:
145
+ # Calculate difficulty distribution
146
+ difficulties = [r.get('difficulty', 'Unknown') for r in recommendations]
147
+ easy_count = difficulties.count('Bardzo łatwe') + difficulties.count('Łatwe')
148
+ difficulty_text = f"{easy_count}/{len(recommendations)} Easy"
149
+ st.metric(
150
+ "Difficulty",
151
+ difficulty_text,
152
+ help="How many recommendations are easy to implement"
153
+ )
154
+
155
+ st.markdown("---")
156
+
157
+ # Group recommendations by priority
158
+ high_priority = [r for r in recommendations if r.get('priority', 5) <= 2]
159
+ medium_priority = [r for r in recommendations if r.get('priority', 5) == 3]
160
+ low_priority = [r for r in recommendations if r.get('priority', 5) >= 4]
161
+
162
+ # Display high priority recommendations
163
+ if high_priority:
164
+ st.markdown("### 🚨 High Priority (Do First)")
165
+ for rec in high_priority:
166
+ display_recommendation_card(rec)
167
+
168
+ # Display medium priority recommendations
169
+ if medium_priority:
170
+ st.markdown("### ⚡ Medium Priority")
171
+ for rec in medium_priority:
172
+ display_recommendation_card(rec)
173
+
174
+ # Display low priority recommendations
175
+ if low_priority:
176
+ st.markdown("### 💡 Additional Improvements")
177
+ for rec in low_priority:
178
+ display_recommendation_card(rec)
179
+
180
+ def display_recommendation_card(recommendation: Dict):
181
+ """Display a single recommendation as a card"""
182
+
183
+ # Priority styling
184
+ priority = recommendation.get('priority', 5)
185
+ if priority <= 2:
186
+ border_color = "#ff4b4b" # Red
187
+ elif priority == 3:
188
+ border_color = "#ffa500" # Orange
189
+ else:
190
+ border_color = "#1f77b4" # Blue
191
+
192
+ # Card styling
193
+ st.markdown(f"""
194
+ <div style="
195
+ border: 2px solid {border_color};
196
+ border-radius: 10px;
197
+ padding: 15px;
198
+ margin: 10px 0;
199
+ background-color: white;
200
+ ">
201
+ </div>
202
+ """, unsafe_allow_html=True)
203
+
204
+ with st.container():
205
+ col1, col2 = st.columns([3, 1])
206
+
207
+ with col1:
208
+ # Title and description
209
+ title = recommendation.get('title', 'Recommendation')
210
+ st.markdown(f"**{title}**")
211
+
212
+ description = recommendation.get('description', '')
213
+ if description:
214
+ st.markdown(f"*{description}*")
215
+
216
+ # Action
217
+ action = recommendation.get('action', '')
218
+ if action:
219
+ st.markdown(f"**Action:** {action}")
220
+
221
+ with col2:
222
+ # Metrics
223
+ impact = recommendation.get('estimated_impact', '')
224
+ if impact:
225
+ st.markdown(f"📈 **{impact}**")
226
+
227
+ time_to_fix = recommendation.get('time_to_fix', '')
228
+ if time_to_fix:
229
+ st.markdown(f"⏱️ {time_to_fix}")
230
+
231
+ difficulty = recommendation.get('difficulty', '')
232
+ if difficulty:
233
+ difficulty_emoji = {
234
+ 'Bardzo łatwe': '🟢',
235
+ 'Łatwe': '🟡',
236
+ 'Średnie': '🟠',
237
+ 'Trudne': '🔴'
238
+ }.get(difficulty, '⚪')
239
+ st.markdown(f"{difficulty_emoji} {difficulty}")
240
+
241
+ def display_analysis_history(history: List[Dict]):
242
+ """Display analysis history"""
243
+
244
+ if not history:
245
+ st.info("No analysis history available.")
246
+ return
247
+
248
+ for i, analysis in enumerate(reversed(history[-10:])): # Show last 10
249
+ with st.expander(
250
+ f"{analysis.get('product_name', 'Unknown')} - {analysis.get('analysis_date', 'Unknown')}"
251
+ ):
252
+ results = analysis.get('results', {})
253
+
254
+ col1, col2, col3 = st.columns(3)
255
+
256
+ with col1:
257
+ score = results.get('overall_score', 'N/A')
258
+ st.metric("Score", f"{score}/10")
259
+
260
+ with col2:
261
+ found = results.get('product_found', False)
262
+ status = "Found" if found else "Not Found"
263
+ st.metric("Status", status)
264
+
265
+ with col3:
266
+ facings = results.get('facing_count', 0)
267
+ st.metric("Facings", facings)
268
+
269
+ description = results.get('description', '')
270
+ if description:
271
+ st.markdown(f"**Description:** {description[:200]}...")
272
+
273
+ def create_export_summary(analysis_results: Dict, recommendations: List[Dict], product_name: str) -> str:
274
+ """Create a summary for export purposes"""
275
+
276
+ summary = f"""
277
+ # Shelf Analysis Report - {product_name}
278
+
279
+ ## Analysis Results
280
+ - **Overall Score:** {analysis_results.get('overall_score', 'N/A')}/10
281
+ - **Product Found:** {'Yes' if analysis_results.get('product_found') else 'No'}
282
+ - **Facing Count:** {analysis_results.get('facing_count', 0)}
283
+ - **Shelf Position:** {analysis_results.get('shelf_position', 'Unknown')}
284
+ - **Price Visible:** {'Yes' if analysis_results.get('price_visible') else 'No'}
285
+ - **Product Condition:** {analysis_results.get('product_condition', 'Unknown')}
286
+ - **AI Confidence:** {int(analysis_results.get('confidence', 0.5) * 100)}%
287
+
288
+ ## Recommendations ({len(recommendations)})
289
+ """
290
+
291
+ for i, rec in enumerate(recommendations, 1):
292
+ summary += f"""
293
+ ### {i}. {rec.get('title', 'Recommendation')}
294
+ - **Priority:** {rec.get('priority', 'N/A')}
295
+ - **Description:** {rec.get('description', '')}
296
+ - **Action:** {rec.get('action', '')}
297
+ - **Estimated Impact:** {rec.get('estimated_impact', '')}
298
+ - **Time to Fix:** {rec.get('time_to_fix', '')}
299
+ - **Difficulty:** {rec.get('difficulty', '')}
300
+ """
301
+
302
+ return summary