Spaces:
Sleeping
Sleeping
Upload 38 files
Browse files- .gitattributes +6 -0
- src/data.csv +31 -0
- src/data_with_text.csv +77 -0
- src/logo.png +3 -0
- src/pages/newprod.py +451 -0
- src/pages/persona.py +410 -0
- src/pages/prt111.py +700 -0
- src/personas.json +42 -0
- src/review_files/ 1.txt +2 -0
- src/review_files/ 2.txt +2 -0
- src/review_files/ 3.txt +2 -0
- src/review_files/ 4.txt +2 -0
- src/review_files/ 5.txt +2 -0
- src/review_files/ 6.txt +2 -0
- src/review_files/ 7.txt +2 -0
- src/review_files/ 8.txt +2 -0
- src/review_files/ 9.txt +2 -0
- src/review_files/.DS_Store +0 -0
- src/review_files/10.txt +2 -0
- src/review_files/11.txt +2 -0
- src/review_files/12.txt +2 -0
- src/review_files/13.txt +2 -0
- src/review_files/14.txt +2 -0
- src/review_files/15.txt +2 -0
- src/review_files/16.txt +2 -0
- src/review_files/17.txt +2 -0
- src/review_files/18.txt +2 -0
- src/review_files/19.txt +2 -0
- src/review_files/20.txt +2 -0
- src/review_files/21.png +0 -0
- src/review_files/22.png +0 -0
- src/review_files/23.png +0 -0
- src/review_files/24.png +0 -0
- src/review_files/25.png +0 -0
- src/review_files/26.wav +3 -0
- src/review_files/27.wav +3 -0
- src/review_files/28.wav +3 -0
- src/review_files/29.wav +3 -0
- src/review_files/30.wav +3 -0
.gitattributes
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
src/logo.png filter=lfs diff=lfs merge=lfs -text
|
2 |
+
src/review_files/26.wav filter=lfs diff=lfs merge=lfs -text
|
3 |
+
src/review_files/27.wav filter=lfs diff=lfs merge=lfs -text
|
4 |
+
src/review_files/28.wav filter=lfs diff=lfs merge=lfs -text
|
5 |
+
src/review_files/29.wav filter=lfs diff=lfs merge=lfs -text
|
6 |
+
src/review_files/30.wav filter=lfs diff=lfs merge=lfs -text
|
src/data.csv
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
unique_order_id,customer_id,product_name,price,review_file,timestamp
|
2 |
+
1001,5001,Chocolate flavoured whey 1kg,1100,1.txt,2025-07-02 13:40:09
|
3 |
+
1002,5002,Chocolate flavoured whey 1kg,1300,2.txt,2025-06-30 01:44:41
|
4 |
+
1003,5003,Chocolate flavoured whey 1kg,1250,3.txt,2025-07-02 16:53:24
|
5 |
+
1004,5004,Chocolate flavoured whey 1kg,1300,4.txt,2025-06-29 15:29:25
|
6 |
+
1005,5005,Chocolate flavoured whey 1kg,1300,5.txt,2025-06-29 13:21:02
|
7 |
+
1006,5006,Chocolate flavoured whey 1kg,1100,6.txt,2025-06-27 12:36:57
|
8 |
+
1007,5007,Chocolate flavoured whey 1kg,1250,7.txt,2025-07-01 20:21:00
|
9 |
+
1008,5008,Chocolate flavoured whey 1kg,1100,8.txt,2025-07-02 04:35:40
|
10 |
+
1009,5009,Chocolate flavoured whey 1kg,1250,9.txt,2025-07-02 12:56:36
|
11 |
+
1010,5010,Chocolate flavoured whey 1kg,1300,10.txt,2025-06-30 12:45:43
|
12 |
+
1011,5011,Chocolate flavoured whey 1kg,1250,11.txt,2025-06-27 08:43:25
|
13 |
+
1012,5012,Chocolate flavoured whey 1kg,1300,12.txt,2025-06-30 12:02:19
|
14 |
+
1013,5013,Chocolate flavoured whey 1kg,1200,13.txt,2025-07-02 07:31:49
|
15 |
+
1014,5014,Chocolate flavoured whey 1kg,1250,14.txt,2025-06-27 04:10:51
|
16 |
+
1015,5015,Chocolate flavoured whey 1kg,1100,15.txt,2025-06-28 06:01:28
|
17 |
+
1016,5016,Chocolate flavoured whey 1kg,1100,16.txt,2025-07-01 13:51:04
|
18 |
+
1017,5017,Chocolate flavoured whey 1kg,1250,17.txt,2025-06-26 20:17:45
|
19 |
+
1018,5018,Chocolate flavoured whey 1kg,1200,18.txt,2025-06-30 08:12:20
|
20 |
+
1019,5019,Chocolate flavoured whey 1kg,1250,19.txt,2025-07-01 23:20:05
|
21 |
+
1020,5020,Chocolate flavoured whey 1kg,1250,20.txt,2025-06-26 12:46:36
|
22 |
+
1021,5021,Chocolate flavoured whey 1kg,1200,21.png,2025-06-29 12:06:12
|
23 |
+
1022,5022,Chocolate flavoured whey 1kg,1200,22.png,2025-06-28 12:41:00
|
24 |
+
1023,5023,Chocolate flavoured whey 1kg,1100,23.png,2025-06-30 00:08:33
|
25 |
+
1024,5024,Chocolate flavoured whey 1kg,1300,24.png,2025-07-01 06:10:49
|
26 |
+
1025,5025,Chocolate flavoured whey 1kg,1300,25.png,2025-06-26 05:24:20
|
27 |
+
1026,5026,Chocolate flavoured whey 1kg,1200,26.wav,2025-06-28 15:00:32
|
28 |
+
1027,5027,Chocolate flavoured whey 1kg,1200,27.wav,2025-06-28 06:51:25
|
29 |
+
1028,5028,Chocolate flavoured whey 1kg,1300,28.wav,2025-06-26 01:31:07
|
30 |
+
1029,5029,Chocolate flavoured whey 1kg,1100,29.wav,2025-06-30 04:45:59
|
31 |
+
1030,5030,Chocolate flavoured whey 1kg,1250,30.wav,2025-06-26 15:09:14
|
src/data_with_text.csv
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
unique_order_id,customer_id,product_name,price,review_file,timestamp,review_text,polarity
|
2 |
+
1001,5001,Chocolate flavoured whey 1kg,1100,1.txt,2025-07-02 13:40:09,"Mary says:
|
3 |
+
Improved my stamina noticeably. I feel more energetic during workouts. Good consistency and not too sweet.",0.9996833801269531
|
4 |
+
1002,5002,Chocolate flavoured whey 1kg,1300,2.txt,2025-06-30 01:44:41,"Christopher says:
|
5 |
+
Great taste and mixes well. The flavor is okay, nothing special.",0.9987167119979858
|
6 |
+
1003,5003,Chocolate flavoured whey 1kg,1250,3.txt,2025-07-02 16:53:24,"Maria says:
|
7 |
+
I feel more energetic during workouts. Noticeable muscle recovery improvement.",0.9971129894256592
|
8 |
+
1004,5004,Chocolate flavoured whey 1kg,1300,4.txt,2025-06-29 15:29:25,"Dawn says:
|
9 |
+
Blends easily with water or milk. Very effective for post-workout nutrition. Slight aftertaste but manageable. Improved my stamina noticeably.",0.9991833567619324
|
10 |
+
1005,5005,Chocolate flavoured whey 1kg,1300,5.txt,2025-06-29 13:21:02,"Amy says:
|
11 |
+
Perfect for daily supplementation. Could have a few more flavor options. No digestive issues so far. Very effective for post-workout nutrition.",0.999066174030304
|
12 |
+
1006,5006,Chocolate flavoured whey 1kg,1100,6.txt,2025-06-27 12:36:57,"Stephanie says:
|
13 |
+
Bit pricey for the quantity. I feel more energetic during workouts. The flavor is okay, nothing special.",0.9735181331634521
|
14 |
+
1007,5007,Chocolate flavoured whey 1kg,1250,7.txt,2025-07-01 20:21:00,"Janet says:
|
15 |
+
Mild smell, not unpleasant though. Noticeable muscle recovery improvement. Very effective for post-workout nutrition. I feel more energetic during workouts.",0.997998058795929
|
16 |
+
1008,5008,Chocolate flavoured whey 1kg,1100,8.txt,2025-07-02 04:35:40,"Robin says:
|
17 |
+
Good consistency and not too sweet. Works fine if you’re consistent.",0.9997614026069641
|
18 |
+
1009,5009,Chocolate flavoured whey 1kg,1250,9.txt,2025-07-02 12:56:36,"Cynthia says:
|
19 |
+
Not as filling as expected. Serving scoop could be better marked. Noticeable muscle recovery improvement. Improved my stamina noticeably.",0.7413376569747925
|
20 |
+
1010,5010,Chocolate flavoured whey 1kg,1300,10.txt,2025-06-30 12:45:43,"Stanley says:
|
21 |
+
Good consistency and not too sweet. Very effective for post-workout nutrition. Packaging could be more durable.",0.9992110729217529
|
22 |
+
1011,5011,Chocolate flavoured whey 1kg,1250,11.txt,2025-06-27 08:43:25,"Kyle says:
|
23 |
+
Good consistency and not too sweet. Blends easily with water or milk.",0.9993370175361633
|
24 |
+
1012,5012,Chocolate flavoured whey 1kg,1300,12.txt,2025-06-30 12:02:19,"Angela says:
|
25 |
+
Bit pricey for the quantity. Good consistency and not too sweet. Texture is decent, not too gritty. Helped me maintain my protein intake.",0.9987452030181885
|
26 |
+
1013,5013,Chocolate flavoured whey 1kg,1200,13.txt,2025-07-02 07:31:49,"Todd says:
|
27 |
+
Could have a few more flavor options. Perfect for daily supplementation.",0.9952136278152466
|
28 |
+
1014,5014,Chocolate flavoured whey 1kg,1250,14.txt,2025-06-27 04:10:51,"Thomas says:
|
29 |
+
No digestive issues so far. Bit pricey for the quantity. Could have a few more flavor options.",-0.9891797304153442
|
30 |
+
1015,5015,Chocolate flavoured whey 1kg,1100,15.txt,2025-06-28 06:01:28,"Julia says:
|
31 |
+
Noticeable muscle recovery improvement. Blends easily with water or milk. Blends easily with water or milk. Seems effective but too early to judge.",-0.798577070236206
|
32 |
+
1016,5016,Chocolate flavoured whey 1kg,1100,16.txt,2025-07-01 13:51:04,"Evelyn says:
|
33 |
+
Perfect for daily supplementation. Perfect for daily supplementation. The flavor is okay, nothing special. Blends easily with water or milk.",0.962000846862793
|
34 |
+
1017,5017,Chocolate flavoured whey 1kg,1250,17.txt,2025-06-26 20:17:45,"Bob says:
|
35 |
+
Blends easily with water or milk. Perfect for daily supplementation. Clumps if not shaken properly.",0.986446738243103
|
36 |
+
1018,5018,Chocolate flavoured whey 1kg,1200,18.txt,2025-06-30 08:12:20,"Richard says:
|
37 |
+
Great taste and mixes well. Not as filling as expected. Helped me maintain my protein intake. Very effective for post-workout nutrition.",0.9984906911849976
|
38 |
+
1019,5019,Chocolate flavoured whey 1kg,1250,19.txt,2025-07-01 23:20:05,"Aaron says:
|
39 |
+
Good consistency and not too sweet. Great taste and mixes well.",0.9998080134391785
|
40 |
+
1020,5020,Chocolate flavoured whey 1kg,1250,20.txt,2025-06-26 12:46:36,"Shelby says:
|
41 |
+
Noticeable muscle recovery improvement. Noticeable muscle recovery improvement.",0.9959927797317505
|
42 |
+
1021,5021,Chocolate flavoured whey 1kg,1200,21.png,2025-06-29 12:06:12,"Must buy!
|
43 |
+
|
44 |
+
It's a genuine product . I'm a beginner to to the gym . It's been 6 months since | have joined the gym . | have used it twice on the day it got delivered as
|
45 |
+
the taste is just awesome . Best ever taste as if I'm having some chocolate smoothie. It's just too good . Great product just goo for it",0.999661922454834
|
46 |
+
1022,5022,Chocolate flavoured whey 1kg,1200,22.png,2025-06-28 12:41:00,"Wonderful
|
47 |
+
|
48 |
+
Those who are complaining about taste i don't understand what is wrong it's too good i liked chocolate cream just gonna update result i feel gas after
|
49 |
+
having 3 scoop but i won't consume this much now let me update results",-0.9602248668670654
|
50 |
+
1023,5023,Chocolate flavoured whey 1kg,1100,23.png,2025-06-30 00:08:33,"Mind-blowing purchase
|
51 |
+
|
52 |
+
Dude this is amazing, not only gives tha macros also keeps you tummy full for sometimes. | take it 200 ml of milk one scoop daily. | am a Mixed Martial
|
53 |
+
Artists so i don't really have goal to show my muscles, so | can't point out goods and bads about this specific protein from others. Although works good
|
54 |
+
for me.",0.9985721111297607
|
55 |
+
1024,5024,Chocolate flavoured whey 1kg,1300,24.png,2025-07-01 06:10:49,"Value-for-money
|
56 |
+
|
57 |
+
So this protein powder is best for who's looking to lean bulk
|
58 |
+
|
59 |
+
Mixabiltity - 9/10
|
60 |
+
|
61 |
+
| bought chocolate flavor it's got a good taste - 8/10
|
62 |
+
|
63 |
+
You won't get results quickly, you need to maintain the food also and balanced nutrition
|
64 |
+
|
65 |
+
And it's also lab tested and also hygiene without any side effects and no digestive problems with the product
|
66 |
+
Yeah It maintains body at a balanced weight
|
67 |
+
|
68 |
+
And sure thing is you can buy this without hesitating",0.8845929503440857
|
69 |
+
1025,5025,Chocolate flavoured whey 1kg,1300,25.png,2025-06-26 05:24:20,"Terrific purchase
|
70 |
+
|
71 |
+
Very nice product with authenticity code, complete amino profile mentioned, filtration method mentioned.taste is very nice slightly sweeter. Overall good
|
72 |
+
product for beginners. But it has blotting and stomach upset issue",0.9798041582107544
|
73 |
+
1026,5026,Chocolate flavoured whey 1kg,1200,26.wav,2025-06-28 15:00:32,good morning sir thank you for calling what can I assist you with today I have been using the protein powder for a little over a week and I just wanted to give him give some quick feedback about it because I would like to hear experience on it so first of I appreciate the fact that it's not too heavy some powder is make me feel very over full or uncomfortable this but this one feels light and easy to digest but there are any other areas on which we could improve packaging it's very hard to scope the poverty and the container starts running low a wider container or the longest cook can handle it thanks for your valuable suggestions anything else that's it thank you,-0.9288781881332397
|
74 |
+
1027,5027,Chocolate flavoured whey 1kg,1200,27.wav,2025-06-28 06:51:25,good morning sir thank you for calling how can I assist you today I just wanted to give some feedback about the delivery experience it wasn't great honestly I am sorry to hear that what happened exactly can you elaborate so I placed the order and I got an estimated delivery date but it arrive nearly five days late with no updates in between I kept checking the tracking and it didn't move for 3 days I'm really sorry for the inconvenience cost that's definitely not ideal for our system it's ok understand the delay is happen but I think better communication would have helped it email or a text update would made a big difference if the order and come through properly we really appreciate your feedback and will pass this on to a Logistic team,-0.9972971081733704
|
75 |
+
1028,5028,Chocolate flavoured whey 1kg,1300,28.wav,2025-06-26 01:31:07,good afternoon how may I help you today looking for Protein powder for a couple of weeks now and wanted to share some feedback it was about the pricing of course please go ahead the product itself is decent the taste is taste and flexibility of fine but to be honest I feel it's a bit on the expensive site for what it offers I will similar products before but they were slightly more affordable and better as well I see would you say the quality justify the price that is the thing it's not bad but not exceptional either but you could have offered the bundle pricing or a loyalty discount I would probably continue but at the current price it's a bit hard to manage all the expenses with it thank you for really helpful,0.9790260195732117
|
76 |
+
1029,5029,Chocolate flavoured whey 1kg,1100,29.wav,2025-06-30 04:45:59,good morning sir thank you for calling what can I assist you with today I have been using the protein powder for a little over a week and I just wanted to give him give some quick feedback about it because I would like to hear experience on it so first of I appreciate the fact that it's not too heavy some powder is make me feel very over full or uncomfortable this but this one feels light and easy to digest but there are any other areas on which we could improve packaging it's very hard to scope the poverty and the container starts running low a wider container or the longest cook can handle it thanks for your valuable suggestions anything else that's it thank you,-0.9288781881332397
|
77 |
+
1030,5030,Chocolate flavoured whey 1kg,1250,30.wav,2025-06-26 15:09:14,thank you for calling customer support how may I help you today looking for a Protein powder for about a week now and I just wanted to share a bit of a feedback it's not a major issue but I think it's worth mentioning of course I am happy to hear thoughts about it what seems to be a concern while the protein powder doesn't make as smoothly as I expected even when I use the Shaker bottle I still get a few small lumps it's not the end of the world but it makes the texture a bit of I understand your problem have you tried using a Blender or adding it gradually in the proper amount a little but not so much but I think the protein powder itself could be a little more final all the taste is alright not too bad but just a bit too sweet for me personally I know that subjective but maybe offering an unsuitable version could also be nice thank you for a valuable feedback anything else know that the working of find otherwise not digesting just thought I would share it could be more improved in the future batches thank you,0.9952450394630432
|
src/logo.png
ADDED
![]() |
Git LFS Details
|
src/pages/newprod.py
ADDED
@@ -0,0 +1,451 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import json
|
3 |
+
import os
|
4 |
+
import numpy as np
|
5 |
+
import plotly.graph_objs as go
|
6 |
+
from groq import Groq
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
load_dotenv() # load .env file
|
9 |
+
|
10 |
+
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
11 |
+
|
12 |
+
# --- CONFIG ---
|
13 |
+
GROQ_MODEL = "llama3-70b-8192"
|
14 |
+
groq_client = Groq(api_key=GROQ_API_KEY)
|
15 |
+
PERSONA_PATH = "personas.json"
|
16 |
+
|
17 |
+
# --- THEME COLORS ---
|
18 |
+
neon_blue = "#00fff7"
|
19 |
+
neon_green = "#7CFC00"
|
20 |
+
neon_pink = "#F72585"
|
21 |
+
neon_cyan = "#0ffcff"
|
22 |
+
neon_bg = "#181830"
|
23 |
+
neon_orange = "#FFB347"
|
24 |
+
neon_shadow = "#2dfdff44"
|
25 |
+
|
26 |
+
font_main = "Inter, Segoe UI, Arial, sans-serif"
|
27 |
+
|
28 |
+
st.set_page_config(page_title="🚀 New Launch Studio", layout="wide", initial_sidebar_state="collapsed")
|
29 |
+
|
30 |
+
# --- STYLE ---
|
31 |
+
st.markdown(f"""
|
32 |
+
<style>
|
33 |
+
html, body, [class*="css"] {{
|
34 |
+
background-color: {neon_bg} !important;
|
35 |
+
font-family: {font_main} !important;
|
36 |
+
}}
|
37 |
+
/* HEADERS */
|
38 |
+
.neon-title {{
|
39 |
+
font-size:2.8rem; font-weight:900; color:{neon_blue};
|
40 |
+
letter-spacing:0.02em; margin-bottom:7px; margin-top:6px;
|
41 |
+
text-shadow:0 2px 24px {neon_blue}33;
|
42 |
+
}}
|
43 |
+
.neon-sub {{
|
44 |
+
font-size:1.25rem;font-weight:600;color:#fff;
|
45 |
+
margin-bottom:2px;margin-top:0px;
|
46 |
+
}}
|
47 |
+
.neon-heads-up {{
|
48 |
+
font-size:1.08rem;color:{neon_pink};font-weight:700;margin-bottom:32px;
|
49 |
+
margin-top:7px;
|
50 |
+
}}
|
51 |
+
|
52 |
+
/* BUTTONS */
|
53 |
+
.neon-btn {{
|
54 |
+
display:inline-block;
|
55 |
+
font-weight:bold;
|
56 |
+
padding:13px 32px;
|
57 |
+
border:none;
|
58 |
+
border-radius:13px;
|
59 |
+
font-size:1.10em;
|
60 |
+
margin-right:16px;
|
61 |
+
cursor:pointer;
|
62 |
+
box-shadow:0 0 13px {neon_blue}55;
|
63 |
+
color:#1d1d1d !important;
|
64 |
+
background:linear-gradient(90deg,{neon_green}, {neon_blue});
|
65 |
+
text-decoration:none !important;
|
66 |
+
transition:transform 0.10s;
|
67 |
+
}}
|
68 |
+
.neon-btn-pink {{
|
69 |
+
background:linear-gradient(90deg,{neon_pink}, {neon_blue});
|
70 |
+
color:#fff !important;
|
71 |
+
box-shadow:0 0 16px {neon_pink}88;
|
72 |
+
}}
|
73 |
+
.neon-btn:hover {{ transform:scale(1.06); }}
|
74 |
+
|
75 |
+
/* PERSONA NAME BOX */
|
76 |
+
.persona-name-box {{
|
77 |
+
background: linear-gradient(90deg, {neon_blue}, {neon_pink} 80%);
|
78 |
+
color: #15192A;
|
79 |
+
font-size:2.2rem;
|
80 |
+
font-weight:900;
|
81 |
+
border-radius:28px;
|
82 |
+
padding: 12px 40px 10px 25px;
|
83 |
+
margin-bottom:15px;
|
84 |
+
display: inline-block;
|
85 |
+
box-shadow: 0 2px 26px {neon_cyan}99;
|
86 |
+
letter-spacing:0.01em;
|
87 |
+
margin-top:18px;
|
88 |
+
}}
|
89 |
+
|
90 |
+
/* PERSONA CARD CONTENTS */
|
91 |
+
.persona-section-row {{
|
92 |
+
display: flex;
|
93 |
+
gap: 2.5em;
|
94 |
+
margin-bottom: 0;
|
95 |
+
}}
|
96 |
+
|
97 |
+
.persona-section-col {{
|
98 |
+
flex: 1;
|
99 |
+
min-width: 340px;
|
100 |
+
}}
|
101 |
+
|
102 |
+
/* LABELS */
|
103 |
+
.block-label {{
|
104 |
+
font-weight:900;
|
105 |
+
font-size:1.15em;
|
106 |
+
margin-bottom:8px;
|
107 |
+
margin-top:8px;
|
108 |
+
letter-spacing:0.01em;
|
109 |
+
display:flex;
|
110 |
+
align-items:center;
|
111 |
+
gap:0.6em;
|
112 |
+
}}
|
113 |
+
.label-blue {{ color:{neon_blue}; }}
|
114 |
+
.label-green {{ color:{neon_green}; }}
|
115 |
+
.label-pink {{ color:{neon_pink}; }}
|
116 |
+
.label-orange {{ color:{neon_orange}; }}
|
117 |
+
.label-cyan {{ color:{neon_cyan}; }}
|
118 |
+
|
119 |
+
/* BULLET LISTS */
|
120 |
+
ul.insight-list {{
|
121 |
+
margin-top:7px; margin-bottom:16px;
|
122 |
+
padding-left:22px;
|
123 |
+
}}
|
124 |
+
ul.insight-list li {{
|
125 |
+
font-size:1.11em; font-weight:500; color:#fff;
|
126 |
+
margin-bottom:5px; line-height:1.53;
|
127 |
+
}}
|
128 |
+
|
129 |
+
/* INTEREST & NOTIF */
|
130 |
+
.interest-badge {{
|
131 |
+
display:inline-block;
|
132 |
+
background:linear-gradient(90deg, {neon_green}, {neon_blue} 90%);
|
133 |
+
color:#15192A; font-size:1.09em; font-weight:900;
|
134 |
+
border-radius:15px; padding:8px 30px 7px 18px;
|
135 |
+
margin-right:14px;
|
136 |
+
box-shadow:0 0 17px {neon_green}2c;
|
137 |
+
margin-top:10px;
|
138 |
+
}}
|
139 |
+
.notification-block {{
|
140 |
+
background:linear-gradient(90deg,{neon_cyan}44,#232344 96%);
|
141 |
+
border-left:5px solid {neon_blue};
|
142 |
+
padding:17px 23px 17px 23px;
|
143 |
+
border-radius:14px;
|
144 |
+
font-weight:700;
|
145 |
+
color:{neon_blue};
|
146 |
+
font-size:1.06em;
|
147 |
+
line-height:1.45;
|
148 |
+
box-shadow:0 2px 18px {neon_cyan}1a;
|
149 |
+
margin-bottom:8px;
|
150 |
+
margin-top:10px;
|
151 |
+
letter-spacing:0.01em;
|
152 |
+
max-width:430px;
|
153 |
+
min-width: 240px;
|
154 |
+
display: inline-block;
|
155 |
+
}}
|
156 |
+
|
157 |
+
/* CHART/INSIGHT CARDS */
|
158 |
+
.section-card {{
|
159 |
+
background:rgba(23,28,49,0.97);
|
160 |
+
border-radius: 17px;
|
161 |
+
box-shadow:0 0 22px {neon_cyan}32;
|
162 |
+
padding: 34px 42px 22px 42px;
|
163 |
+
margin-bottom:36px;
|
164 |
+
margin-top:16px;
|
165 |
+
}}
|
166 |
+
|
167 |
+
/* COMBINED INSIGHTS */
|
168 |
+
.insight-box {{
|
169 |
+
background:rgba(23,28,49,0.98);
|
170 |
+
border-radius: 18px;
|
171 |
+
box-shadow:0 0 26px {neon_blue}45;
|
172 |
+
padding: 32px 34px 18px 34px;
|
173 |
+
margin-bottom:33px;
|
174 |
+
margin-top:20px;
|
175 |
+
}}
|
176 |
+
|
177 |
+
/* SUMMARY BOX */
|
178 |
+
.summary-box {{
|
179 |
+
background:rgba(23,28,49,0.97);
|
180 |
+
border-radius: 15px;
|
181 |
+
box-shadow:0 0 22px {neon_green}32;
|
182 |
+
padding: 32px 38px 22px 38px;
|
183 |
+
margin-bottom:36px;
|
184 |
+
margin-top:14px;
|
185 |
+
color:#fff;
|
186 |
+
font-size:1.17em;
|
187 |
+
}}
|
188 |
+
|
189 |
+
/* RESPONSIVE */
|
190 |
+
@media (max-width: 1000px) {{
|
191 |
+
.persona-section-row {{ flex-direction: column; }}
|
192 |
+
.persona-section-col {{ min-width: 100%; }}
|
193 |
+
}}
|
194 |
+
</style>
|
195 |
+
""", unsafe_allow_html=True)
|
196 |
+
|
197 |
+
# --- TITLE & DESCRIPTION ---
|
198 |
+
st.markdown(f"<div class='neon-title'>🚀 New Launch Studio</div>", unsafe_allow_html=True)
|
199 |
+
st.markdown(f"<div class='neon-sub'>Will your next product idea actually vibe with your audience? Pop your concept below and instantly see what your customer personas think—no fluff, just punchy, actionable feedback and a reality check on your launch.</div>", unsafe_allow_html=True)
|
200 |
+
st.markdown(f"<div class='neon-heads-up'>⚡ Heads up: Our demo and market data is based on protein powder reviews—so for best results, enter a health, nutrition, or supplement product!</div>", unsafe_allow_html=True)
|
201 |
+
|
202 |
+
# --- NAVIGATION BUTTONS ---
|
203 |
+
st.markdown(f"""
|
204 |
+
<div style="display:flex;gap:2em;justify-content:flex-start;margin-bottom:6px;">
|
205 |
+
<a href="/prt111" class="neon-btn" target="_self">🏠 Home</a>
|
206 |
+
<a href="/persona" class="neon-btn neon-btn-pink" target="_self">👤 Persona Analysis</a>
|
207 |
+
</div>
|
208 |
+
""", unsafe_allow_html=True)
|
209 |
+
|
210 |
+
# --- PRODUCT DESCRIPTION INPUT ---
|
211 |
+
st.markdown(f"<h2 style='color:{neon_blue};font-size:2.04rem;font-weight:900;margin-top:30px;margin-bottom:7px;'>1. Describe Your New Product</h2>", unsafe_allow_html=True)
|
212 |
+
product_desc = st.text_area(
|
213 |
+
"",
|
214 |
+
height=110,
|
215 |
+
placeholder="E.g. Introducing VanillaWhey: zero sugar, 25g protein, added digestive enzymes, eco-packaging, smooth vanilla flavor, perfect for fitness and daily wellness."
|
216 |
+
)
|
217 |
+
|
218 |
+
# --- LOAD PERSONAS ---
|
219 |
+
if os.path.exists(PERSONA_PATH):
|
220 |
+
with open(PERSONA_PATH, "r", encoding="utf-8") as f:
|
221 |
+
personas = json.load(f)
|
222 |
+
else:
|
223 |
+
personas = []
|
224 |
+
st.warning("No personas found. Please generate personas first in the Persona Analysis page.")
|
225 |
+
|
226 |
+
def clean_points(text, max_points=2):
|
227 |
+
lines = [l for l in text.replace('\r', '\n').split('\n') if l.strip() and not l.strip().lower().startswith(
|
228 |
+
('here is', 'here are', 'persona:', 'this is', 'for this persona', 'concerns:', 'the following', 'alignment:', '*', 'point'))]
|
229 |
+
points = []
|
230 |
+
for l in lines:
|
231 |
+
l = l.lstrip('-•1234567890. ').strip()
|
232 |
+
if l and len(points) < max_points:
|
233 |
+
points.append(l)
|
234 |
+
return points if points else [text.strip()]
|
235 |
+
|
236 |
+
def ai_points(prompt, max_points=2, max_tokens=120):
|
237 |
+
try:
|
238 |
+
chat_completion = groq_client.chat.completions.create(
|
239 |
+
model=GROQ_MODEL,
|
240 |
+
messages=[
|
241 |
+
{"role": "system",
|
242 |
+
"content": f"You are a market research strategist. Reply with ONLY exactly {max_points} very brief, but fully written bullet points—no intros, no repetition, no generic phrases. Each point should be a full, clear sentence. Never add 'Here are' or any extra intro. Dont mention any names."},
|
243 |
+
{"role": "user", "content": prompt}
|
244 |
+
],
|
245 |
+
max_tokens=max_tokens, temperature=0.7, stop=None
|
246 |
+
)
|
247 |
+
return clean_points(chat_completion.choices[0].message.content.strip(), max_points)
|
248 |
+
except Exception as e:
|
249 |
+
return [f"Error: {e}"]
|
250 |
+
|
251 |
+
def ai_notification(prompt, max_tokens=44):
|
252 |
+
try:
|
253 |
+
chat_completion = groq_client.chat.completions.create(
|
254 |
+
model=GROQ_MODEL,
|
255 |
+
messages=[
|
256 |
+
{"role": "system",
|
257 |
+
"content": "You are a copywriter. Write a single, short, energetic notification or email (max 30 words, no names, no symbols), ending with a call-to-action. Make it stand out and complete."},
|
258 |
+
{"role": "user", "content": prompt}
|
259 |
+
],
|
260 |
+
max_tokens=max_tokens, temperature=0.72, stop=None
|
261 |
+
)
|
262 |
+
return chat_completion.choices[0].message.content.strip().replace("**", "")
|
263 |
+
except Exception as e:
|
264 |
+
return f"Error: {e}"
|
265 |
+
|
266 |
+
def ai_percent(prompt):
|
267 |
+
try:
|
268 |
+
chat_completion = groq_client.chat.completions.create(
|
269 |
+
model=GROQ_MODEL,
|
270 |
+
messages=[{"role": "system", "content": "You are a market research strategist."}, {"role": "user", "content": prompt}],
|
271 |
+
max_tokens=8, temperature=0.25
|
272 |
+
)
|
273 |
+
s = chat_completion.choices[0].message.content.strip()
|
274 |
+
percent = ''.join([c for c in s if c.isdigit()])
|
275 |
+
return percent + "%" if percent else s
|
276 |
+
except Exception as e:
|
277 |
+
return "?"
|
278 |
+
|
279 |
+
def ai_graph_insights(prompt, max_tokens=160):
|
280 |
+
try:
|
281 |
+
chat_completion = groq_client.chat.completions.create(
|
282 |
+
model=GROQ_MODEL,
|
283 |
+
messages=[
|
284 |
+
{"role": "system",
|
285 |
+
"content": "You are a market analyst. Give only 4 numbered, very concise but meaningful insights in separate sentences, no intro line or extra formatting, no 'Here are', no asterisks or stars, just the facts."},
|
286 |
+
{"role": "user", "content": prompt}
|
287 |
+
],
|
288 |
+
max_tokens=max_tokens, temperature=0.7, stop=None
|
289 |
+
)
|
290 |
+
# Always keep only 4, no prefix text
|
291 |
+
lines = [l.lstrip('-•1234567890. ').strip().replace("**", "") for l in chat_completion.choices[0].message.content.strip().split('\n') if l.strip()]
|
292 |
+
return lines[:4]
|
293 |
+
except Exception as e:
|
294 |
+
return [f"Error: {e}"]
|
295 |
+
|
296 |
+
def ai_summary(prompt, max_tokens=90):
|
297 |
+
try:
|
298 |
+
chat_completion = groq_client.chat.completions.create(
|
299 |
+
model=GROQ_MODEL,
|
300 |
+
messages=[
|
301 |
+
{"role": "system",
|
302 |
+
"content": "Write a concise, professional executive summary in 3 sentences. No intro lines, no 'Here is', no asterisks. Be direct and to the point."},
|
303 |
+
{"role": "user", "content": prompt}
|
304 |
+
],
|
305 |
+
max_tokens=max_tokens, temperature=0.7, stop=None
|
306 |
+
)
|
307 |
+
return chat_completion.choices[0].message.content.strip().replace("**", "")
|
308 |
+
except Exception as e:
|
309 |
+
return f"Error: {e}"
|
310 |
+
|
311 |
+
st.markdown("<div style='height:16px;'></div>", unsafe_allow_html=True)
|
312 |
+
|
313 |
+
# --- GENERATE BUTTON ---
|
314 |
+
test_btn = st.button(
|
315 |
+
"🚦 Run Persona–Product Fit Check",
|
316 |
+
help="Instantly see AI-powered feedback from every persona's perspective!",
|
317 |
+
use_container_width=True
|
318 |
+
)
|
319 |
+
st.markdown("<div style='height:12px;'></div>", unsafe_allow_html=True)
|
320 |
+
|
321 |
+
if test_btn and product_desc and personas:
|
322 |
+
st.markdown(f"<h2 style='color:{neon_blue};font-size:2.23rem;font-weight:900;margin-bottom:12px;margin-top:17px;'>2. Persona-by-Persona Results</h2>", unsafe_allow_html=True)
|
323 |
+
persona_colors = [neon_blue, neon_green, neon_pink, neon_orange, neon_cyan]
|
324 |
+
persona_cycle = iter(persona_colors)
|
325 |
+
section_icons = {
|
326 |
+
"Probable Reaction": "💡",
|
327 |
+
"Alignment with Persona": "✅",
|
328 |
+
"Potential Mismatches or Concerns": "⚠️",
|
329 |
+
"Marketing Strategy": "📢",
|
330 |
+
"Personalized Notification": "🔔",
|
331 |
+
}
|
332 |
+
|
333 |
+
def persona_block(persona, color):
|
334 |
+
return st.container()
|
335 |
+
|
336 |
+
# Pair personas 2 per row
|
337 |
+
for i in range(0, len(personas), 2):
|
338 |
+
cols = st.columns(2, gap="large")
|
339 |
+
for j, col in enumerate(cols):
|
340 |
+
if i + j < len(personas):
|
341 |
+
persona = personas[i + j]
|
342 |
+
color = next(persona_cycle, neon_blue)
|
343 |
+
with col:
|
344 |
+
st.markdown(f"<div class='persona-name-box' style='background:linear-gradient(90deg,{neon_blue},{neon_pink} 80%);margin-bottom:16px;'><span>{persona.get('icon','')} {persona['name']}</span></div>", unsafe_allow_html=True)
|
345 |
+
st.markdown(f"<div style='height:4px;'></div>", unsafe_allow_html=True)
|
346 |
+
st.markdown("<div class='persona-section-row'>", unsafe_allow_html=True)
|
347 |
+
st.markdown("<div class='persona-section-col'>", unsafe_allow_html=True)
|
348 |
+
st.markdown(f"<div class='block-label label-blue'>{section_icons['Probable Reaction']} Probable Reaction</div>", unsafe_allow_html=True)
|
349 |
+
reactions = ai_points(
|
350 |
+
f"Summarize two brief but complete points for this persona's likely reaction to the product: {product_desc}. Use clear, direct language.",
|
351 |
+
max_points=2, max_tokens=90
|
352 |
+
)
|
353 |
+
st.markdown(f"<ul class='insight-list'>" + "".join([f"<li>{r}</li>" for r in reactions]) + "</ul>", unsafe_allow_html=True)
|
354 |
+
st.markdown(f"<div class='block-label label-green'>{section_icons['Alignment with Persona']} Alignment with Persona</div>", unsafe_allow_html=True)
|
355 |
+
aligns = ai_points(
|
356 |
+
f"List two specific ways this persona's characteristics or needs will match with the features or benefits of the product: {product_desc}. "
|
357 |
+
f"Be explicit: mention which part of the persona is satisfied by which product feature. Use clear, direct language.",
|
358 |
+
max_points=2, max_tokens=100
|
359 |
+
)
|
360 |
+
st.markdown(f"<ul class='insight-list'>" + "".join([f"<li>{a}</li>" for a in aligns]) + "</ul>", unsafe_allow_html=True)
|
361 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
362 |
+
st.markdown("<div class='persona-section-col'>", unsafe_allow_html=True)
|
363 |
+
st.markdown(f"<div class='block-label label-pink'>{section_icons['Potential Mismatches or Concerns']} Potential Mismatches or Concerns</div>", unsafe_allow_html=True)
|
364 |
+
mismatches = ai_points(
|
365 |
+
f"List two precise concerns or mismatches: Which features or aspects of the {product_desc} may NOT align with this persona's preferences or needs? "
|
366 |
+
f"Be explicit: mention which product feature is likely to be a turn-off or ignored by this persona.",
|
367 |
+
max_points=2, max_tokens=100
|
368 |
+
)
|
369 |
+
|
370 |
+
st.markdown(f"<ul class='insight-list'>" + "".join([f"<li>{m}</li>" for m in mismatches]) + "</ul>", unsafe_allow_html=True)
|
371 |
+
st.markdown(f"<div class='block-label label-orange'>{section_icons['Marketing Strategy']} Marketing Strategy</div>", unsafe_allow_html=True)
|
372 |
+
strategy = ai_points(
|
373 |
+
f"Suggest two creative, product-specific marketing strategies targeted at this persona for this product: {product_desc}. "
|
374 |
+
f"Each point must clearly connect a product feature with a unique marketing approach for this persona.",
|
375 |
+
max_points=2, max_tokens=100
|
376 |
+
)
|
377 |
+
|
378 |
+
st.markdown(f"<ul class='insight-list'>" + "".join([f"<li>{s}</li>" for s in strategy]) + "</ul>", unsafe_allow_html=True)
|
379 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
380 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
381 |
+
st.markdown(
|
382 |
+
f"""
|
383 |
+
<div style='display:flex;align-items:center;gap:22px;margin-top:12px;margin-bottom:22px;'>
|
384 |
+
<span class='interest-badge'>Interest Likelihood: {ai_percent('Estimate the likelihood (percent) that '+persona['name']+' would be interested in this product. Just the number and % sign, nothing else.')}</span>
|
385 |
+
<div>
|
386 |
+
<div class='block-label label-cyan' style='margin-bottom:3px;'>{section_icons['Personalized Notification']} Personalized Notification</div>
|
387 |
+
<div class='notification-block'>{ai_notification(
|
388 |
+
f"Write a concise, energetic notification or email about this product: {product_desc} aimed specifically at the persona {persona['name']}. "
|
389 |
+
f"Address their top motivations and finish with a strong call-to-action. No names, no symbols."
|
390 |
+
)}</div>
|
391 |
+
|
392 |
+
|
393 |
+
</div>
|
394 |
+
""", unsafe_allow_html=True
|
395 |
+
)
|
396 |
+
st.markdown("<div style='height:4px;'></div>", unsafe_allow_html=True)
|
397 |
+
|
398 |
+
# --- CHARTS (Demo) ---
|
399 |
+
st.markdown(f"<h2 style='color:{neon_cyan};font-size:2.1rem;font-weight:800;margin-top:32px;'>3. Projected Market Impact</h2>", unsafe_allow_html=True)
|
400 |
+
persona_names = [p['name'] for p in personas]
|
401 |
+
np.random.seed(42)
|
402 |
+
projected_market_share = np.random.dirichlet(np.ones(len(persona_names)), size=1)[0]
|
403 |
+
projected_sentiment = projected_market_share * 0.6 + np.random.rand(len(persona_names)) * 0.4 # correlation
|
404 |
+
|
405 |
+
c1, c2 = st.columns(2)
|
406 |
+
with c1:
|
407 |
+
st.markdown(f"<div style='font-size:1.17em;color:{neon_blue};font-weight:700;margin-bottom:6px;'>Projected Market Share by Persona</div>", unsafe_allow_html=True)
|
408 |
+
fig1 = go.Figure(data=[go.Pie(labels=persona_names, values=projected_market_share, hole=0.45)])
|
409 |
+
fig1.update_traces(textinfo='percent+label')
|
410 |
+
fig1.update_layout(margin=dict(l=14, r=14, b=14, t=14), showlegend=True)
|
411 |
+
st.plotly_chart(fig1, use_container_width=True)
|
412 |
+
|
413 |
+
with c2:
|
414 |
+
st.markdown(f"<div style='font-size:1.17em;color:{neon_orange};font-weight:700;margin-bottom:6px;'>Projected Sentiment by Persona</div>", unsafe_allow_html=True)
|
415 |
+
fig2 = go.Figure(data=[go.Bar(x=persona_names, y=projected_sentiment,
|
416 |
+
marker=dict(color=[neon_green, neon_blue, neon_pink, neon_orange, neon_cyan][:len(persona_names)]))])
|
417 |
+
fig2.update_layout(xaxis_title="Persona", yaxis_title="Projected Sentiment", font=dict(size=15))
|
418 |
+
st.plotly_chart(fig2, use_container_width=True)
|
419 |
+
|
420 |
+
# --- Combined Chart Insights ---
|
421 |
+
combined_prompt = (
|
422 |
+
f"Given the projected market share {list(np.round(projected_market_share*100,1))} percent and projected sentiment {list(np.round(projected_sentiment*100,1))} for these personas: {', '.join(persona_names)}, "
|
423 |
+
"summarize 4 concise points that correlate the two charts and reveal the most important market insights. Each point should be in a new line and fully written."
|
424 |
+
)
|
425 |
+
insights = ai_graph_insights(combined_prompt, max_tokens=200)
|
426 |
+
st.markdown(
|
427 |
+
f"<div class='insight-box'><div style='font-size:1.18em;color:{neon_blue};font-weight:700;margin-bottom:10px;'>Key Combined Insights</div>"
|
428 |
+
f"<ul class='insight-list'>" + "".join([f"<li>{bp}</li>" for bp in insights]) + "</ul></div>", unsafe_allow_html=True
|
429 |
+
)
|
430 |
+
|
431 |
+
# --- OVERALL SUMMARY ---
|
432 |
+
st.markdown(f"<h2 style='color:{neon_green};font-size:2rem;font-weight:900;margin-top:18px;'>4. Overall Summary</h2>", unsafe_allow_html=True)
|
433 |
+
overall_prompt = (
|
434 |
+
f"Given these personas: {', '.join([p['name'] for p in personas])}, and the new product: {product_desc}, "
|
435 |
+
"write a concise executive summary (3 sentences, no intro, no asterisks), focusing on overall fit, the main challenge, and the best next move for launch."
|
436 |
+
)
|
437 |
+
summary_text = ai_summary(overall_prompt, max_tokens=1000)
|
438 |
+
st.markdown(
|
439 |
+
f"<div class='summary-box'>{summary_text}</div>",
|
440 |
+
unsafe_allow_html=True
|
441 |
+
)
|
442 |
+
st.markdown("---")
|
443 |
+
|
444 |
+
elif test_btn:
|
445 |
+
st.warning("Please enter your product description to see the results.")
|
446 |
+
|
447 |
+
# --- FOOTER ---
|
448 |
+
st.markdown(
|
449 |
+
f"<small style='color:{neon_pink};font-size:1.09em;'>Powered by Bugs Fring</small>",
|
450 |
+
unsafe_allow_html=True
|
451 |
+
)
|
src/pages/persona.py
ADDED
@@ -0,0 +1,410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import numpy as np
|
4 |
+
import os
|
5 |
+
import re
|
6 |
+
from groq import Groq
|
7 |
+
import plotly.graph_objs as go
|
8 |
+
from collections import defaultdict
|
9 |
+
from itertools import cycle
|
10 |
+
import json
|
11 |
+
from dotenv import load_dotenv
|
12 |
+
PERSONA_PATH = "personas.json"
|
13 |
+
|
14 |
+
# --- THEME COLORS ---
|
15 |
+
neon_blue = "#00fff7"
|
16 |
+
neon_green = "#7CFC00"
|
17 |
+
neon_pink = "#F72585"
|
18 |
+
neon_yellow = "#FFF600"
|
19 |
+
neon_bg = "#181830"
|
20 |
+
neon_orange = "#FFB347"
|
21 |
+
neon_dark = "#202037"
|
22 |
+
load_dotenv() # load .env file
|
23 |
+
|
24 |
+
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
25 |
+
|
26 |
+
# --- CONFIG ---
|
27 |
+
GROQ_MODEL = "llama3-70b-8192"
|
28 |
+
groq_client = Groq(api_key=GROQ_API_KEY)
|
29 |
+
PRODUCT_CONTEXT = (
|
30 |
+
"You are an AI market research expert analyzing customer reviews for a chocolate-flavoured whey protein powder. "
|
31 |
+
"Generate user personas based on patterns and diversity in the reviews."
|
32 |
+
)
|
33 |
+
CSV_PATH = "/Users/kushagraaatre/Downloads/Texpedition/data_with_text.csv"
|
34 |
+
|
35 |
+
st.set_page_config(page_title="Persona Lab", layout="wide", initial_sidebar_state="collapsed")
|
36 |
+
st.markdown(
|
37 |
+
"<h1 style='color:#00fff7;font-size:2.6rem;font-weight:900;letter-spacing:0.01em;margin-bottom:5px;'>🎭 Persona Lab</h1>",
|
38 |
+
unsafe_allow_html=True
|
39 |
+
)
|
40 |
+
st.markdown(
|
41 |
+
f"""
|
42 |
+
<div style="font-size:1.21rem; color:#AC7CFF; font-weight:600; margin-top:-13px; margin-bottom:14px; line-height:1.5;">
|
43 |
+
Ready to peek inside the minds of your customers?
|
44 |
+
This is your sandbox for uncovering who buys, why they rave, and what they crave—powered by real reviews and sharp AI.
|
45 |
+
Dive in, explore the personas that drive your market, and see your brand through their eyes (and taste buds)!
|
46 |
+
</div>
|
47 |
+
""",
|
48 |
+
unsafe_allow_html=True
|
49 |
+
)
|
50 |
+
|
51 |
+
# --- NAVIGATION BUTTONS ---
|
52 |
+
st.markdown("""
|
53 |
+
<style>
|
54 |
+
.neon-btn {
|
55 |
+
display:inline-block;
|
56 |
+
font-weight:bold;
|
57 |
+
padding:14px 32px;
|
58 |
+
border:none;
|
59 |
+
border-radius:12px;
|
60 |
+
font-size:1.1em;
|
61 |
+
margin-right:18px;
|
62 |
+
cursor:pointer;
|
63 |
+
box-shadow:0 0 14px #00fff777;
|
64 |
+
color:#222 !important;
|
65 |
+
background:linear-gradient(90deg,#7CFC00,#00fff7);
|
66 |
+
text-decoration:none !important;
|
67 |
+
transition: transform 0.08s;
|
68 |
+
}
|
69 |
+
.neon-btn-pink {
|
70 |
+
background:linear-gradient(90deg,#F72585,#00fff7);
|
71 |
+
color:#fff !important;
|
72 |
+
box-shadow:0 0 14px #F7258577;
|
73 |
+
}
|
74 |
+
.neon-btn:hover {
|
75 |
+
transform:scale(1.04);
|
76 |
+
box-shadow:0 0 24px #00fff799;
|
77 |
+
}
|
78 |
+
.neon-btn-pink:hover {
|
79 |
+
box-shadow:0 0 24px #F7258599;
|
80 |
+
}
|
81 |
+
</style>
|
82 |
+
""", unsafe_allow_html=True)
|
83 |
+
|
84 |
+
st.markdown("""
|
85 |
+
<div style="display:flex;gap:2em;justify-content:flex-start;">
|
86 |
+
<a href="/prt111" class="neon-btn"target="_self">🏠 Home</a>
|
87 |
+
<a href="/newprod" class="neon-btn neon-btn-pink"target="_self">🚀 New Product Launch</a>
|
88 |
+
</div>
|
89 |
+
<br>
|
90 |
+
""", unsafe_allow_html=True)
|
91 |
+
|
92 |
+
|
93 |
+
def block_markdown(text, color):
|
94 |
+
text = text.replace('\n', '<br>')
|
95 |
+
return (
|
96 |
+
f'<div style="background:linear-gradient(90deg,{color}22,#181830 90%);'
|
97 |
+
f'padding:16px 22px;border-radius:16px;margin:10px 0 24px 0;'
|
98 |
+
f'font-weight:600;color:#fff;font-size:1.04em;line-height:1.6;box-shadow:0 2px 24px {color}19;">'
|
99 |
+
f'{text}</div>'
|
100 |
+
)
|
101 |
+
|
102 |
+
@st.cache_data(show_spinner=True)
|
103 |
+
def load_reviews(csv_path):
|
104 |
+
if not os.path.exists(csv_path):
|
105 |
+
st.error(f"CSV file not found: {csv_path}")
|
106 |
+
return pd.DataFrame()
|
107 |
+
df = pd.read_csv(csv_path)
|
108 |
+
if "polarity" not in df.columns:
|
109 |
+
try:
|
110 |
+
from transformers import pipeline
|
111 |
+
sa = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english")
|
112 |
+
df["polarity"] = df["review_text"].apply(lambda x: 1 if sa(x)[0]["label"] == "POSITIVE" else -1)
|
113 |
+
except Exception as e:
|
114 |
+
st.warning("Could not compute sentiment scores. All reviews set to neutral (0).")
|
115 |
+
df["polarity"] = 0
|
116 |
+
|
117 |
+
if "review_length" not in df.columns:
|
118 |
+
df["review_length"] = df["review_text"].apply(lambda x: len(str(x).split()))
|
119 |
+
return df
|
120 |
+
|
121 |
+
def generate_personas(review_texts, n_personas=4):
|
122 |
+
prompt = (
|
123 |
+
f"Read the following customer reviews for a chocolate-flavored whey protein powder. "
|
124 |
+
f"Based on the language, interests, and context, segment these users into {n_personas} distinct personas. "
|
125 |
+
"For each persona, provide:\n"
|
126 |
+
"1. Persona Name starting with emoji\n"
|
127 |
+
"2. A one-line summary\n"
|
128 |
+
"3. Five detailed bullet points describing their characteristics, needs, goals, or behaviors (each bullet should be specific and insightful, not generic).\n"
|
129 |
+
"Give the answer as a numbered list, one for each persona. Format:\n"
|
130 |
+
"1. [Emoji] Persona Name\nSummary: ...\n- ...\n- ...\n- ...\n- ...\n- ...\n"
|
131 |
+
"\nREVIEWS:\n" +
|
132 |
+
"\n".join(review_texts[:120])[:3600]
|
133 |
+
)
|
134 |
+
try:
|
135 |
+
chat_completion = groq_client.chat.completions.create(
|
136 |
+
model=GROQ_MODEL,
|
137 |
+
messages=[
|
138 |
+
{"role": "system", "content": PRODUCT_CONTEXT},
|
139 |
+
{"role": "user", "content": prompt}
|
140 |
+
],
|
141 |
+
max_tokens=900,
|
142 |
+
temperature=0.6,
|
143 |
+
)
|
144 |
+
return chat_completion.choices[0].message.content.strip()
|
145 |
+
except Exception as e:
|
146 |
+
return f"Error generating personas: {e}"
|
147 |
+
|
148 |
+
def parse_personas_bulletproof(llm_output, n=4):
|
149 |
+
lines = llm_output.splitlines()
|
150 |
+
persona_headers = []
|
151 |
+
for i, line in enumerate(lines):
|
152 |
+
if re.match(r"^([0-9]{1,2}[.)-]?\s*)?[\U0001F300-\U0001FAFF]", line.strip()):
|
153 |
+
persona_headers.append(i)
|
154 |
+
persona_blocks = []
|
155 |
+
for idx, start in enumerate(persona_headers):
|
156 |
+
end = persona_headers[idx+1] if idx+1 < len(persona_headers) else len(lines)
|
157 |
+
persona_blocks.append(lines[start:end])
|
158 |
+
|
159 |
+
personas = []
|
160 |
+
for block in persona_blocks[:n]:
|
161 |
+
name_line = re.sub(r"^([0-9]{1,2}[.)-]?\s*)?", "", block[0]).strip().replace("**", "")
|
162 |
+
summary = ""
|
163 |
+
bullets = []
|
164 |
+
for l in block[1:]:
|
165 |
+
l = l.strip()
|
166 |
+
if not l: continue
|
167 |
+
if not summary and ("summary" in l.lower() or not l.startswith(("-", "•", "*", "+"))):
|
168 |
+
summary = re.sub(r"^summary[:\- ]*", "", l, flags=re.I)
|
169 |
+
elif l.startswith(("-", "•", "*", "+")) or re.match(r"^[0-9]{1,2}[.)-]", l):
|
170 |
+
b = re.sub(r"^[-•*+0-9. ]+", "", l)
|
171 |
+
if b: bullets.append(b)
|
172 |
+
personas.append({
|
173 |
+
"name": name_line,
|
174 |
+
"summary": summary,
|
175 |
+
"bullets": bullets[:5]
|
176 |
+
})
|
177 |
+
return personas
|
178 |
+
|
179 |
+
def assign_review_to_persona_tfidf(df, persona_defs):
|
180 |
+
# Use TF-IDF cosine similarity for assignment (faster than LLM for large data)
|
181 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
182 |
+
persona_texts = [p["summary"] + " " + " ".join(p["bullets"]) for p in persona_defs]
|
183 |
+
tfidf = TfidfVectorizer(stop_words='english')
|
184 |
+
X = tfidf.fit_transform(df["review_text"].tolist() + persona_texts)
|
185 |
+
review_vecs = X[:-len(persona_texts)]
|
186 |
+
persona_vecs = X[-len(persona_texts):]
|
187 |
+
assignments = []
|
188 |
+
for i in range(review_vecs.shape[0]):
|
189 |
+
sims = review_vecs[i].dot(persona_vecs.T).toarray().flatten()
|
190 |
+
idx = np.argmax(sims)
|
191 |
+
assignments.append(persona_defs[idx]["name"])
|
192 |
+
return assignments
|
193 |
+
|
194 |
+
def groq_bullets_persona(chart_desc, chart_data_text):
|
195 |
+
user_prompt = (
|
196 |
+
f"Summarize as exactly two bullet points the main insights for this chart: {chart_desc}. "
|
197 |
+
f"Here is the data: {chart_data_text}. "
|
198 |
+
"Provide a percentage if applicable. Just facts."
|
199 |
+
)
|
200 |
+
try:
|
201 |
+
chat_completion = groq_client.chat.completions.create(
|
202 |
+
model=GROQ_MODEL,
|
203 |
+
messages=[
|
204 |
+
{"role": "system", "content": PRODUCT_CONTEXT},
|
205 |
+
{"role": "user", "content": user_prompt}
|
206 |
+
],
|
207 |
+
max_tokens=80,
|
208 |
+
temperature=0.5,
|
209 |
+
)
|
210 |
+
bullets = chat_completion.choices[0].message.content.strip()
|
211 |
+
points = [line for line in bullets.splitlines() if line.strip().startswith(("-", "•"))]
|
212 |
+
return "\n".join(points[:2]) if len(points) >= 2 else "- " + bullets
|
213 |
+
except Exception:
|
214 |
+
return "- Summary not available.\n- (LLM error)"
|
215 |
+
|
216 |
+
# --- EMOTION PIPELINE (optional) ---
|
217 |
+
def emotion_pipeline(df):
|
218 |
+
try:
|
219 |
+
from transformers import pipeline
|
220 |
+
emo = pipeline(
|
221 |
+
"text-classification",
|
222 |
+
model="finiteautomata/bertweet-base-emotion-analysis", # much smaller than roberta-base!
|
223 |
+
top_k=None,
|
224 |
+
device=-1 # always use CPU, avoid meta-tensor bug
|
225 |
+
)
|
226 |
+
except Exception as e:
|
227 |
+
st.warning(f"Could not load emotion model, skipping emotion analysis: {e}")
|
228 |
+
df["main_emotion"] = "neutral"
|
229 |
+
return df
|
230 |
+
all_emotions = []
|
231 |
+
for t in df["review_text"]:
|
232 |
+
try:
|
233 |
+
emotions = emo(t[:512])
|
234 |
+
if isinstance(emotions, list) and len(emotions) and isinstance(emotions[0], list):
|
235 |
+
# Sometimes returns list of lists
|
236 |
+
emotions = emotions[0]
|
237 |
+
main_emo = sorted(emotions, key=lambda x: -x["score"])[0]["label"]
|
238 |
+
except Exception:
|
239 |
+
main_emo = "neutral"
|
240 |
+
all_emotions.append(main_emo)
|
241 |
+
df["main_emotion"] = all_emotions
|
242 |
+
return df
|
243 |
+
|
244 |
+
|
245 |
+
# ========== MAIN PIPELINE ========== #
|
246 |
+
|
247 |
+
with st.spinner("🔎 Analyzing your data... Please wait a few moments."):
|
248 |
+
df = load_reviews(CSV_PATH)
|
249 |
+
reviews = df["review_text"].dropna().tolist() if not df.empty else []
|
250 |
+
reviews = [t for t in reviews if "unreadable" not in t and "missing" not in t and t.strip()]
|
251 |
+
if reviews:
|
252 |
+
personas_raw = generate_personas(reviews, 4)
|
253 |
+
personas = parse_personas_bulletproof(personas_raw, 4)
|
254 |
+
if personas:
|
255 |
+
with open(PERSONA_PATH, "w", encoding="utf-8") as f:
|
256 |
+
json.dump(personas, f, ensure_ascii=False, indent=2)
|
257 |
+
st.session_state['personas'] = personas
|
258 |
+
st.success(f"{len(personas)} personas saved for next use.")
|
259 |
+
else:
|
260 |
+
personas = []
|
261 |
+
|
262 |
+
persona_colors = [neon_green, neon_blue, neon_pink, neon_orange]
|
263 |
+
persona_cycler = cycle(persona_colors)
|
264 |
+
persona_blocks = []
|
265 |
+
persona_names = []
|
266 |
+
|
267 |
+
# Persona grid (left-right)
|
268 |
+
if personas:
|
269 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
270 |
+
grid_cols = st.columns(2)
|
271 |
+
for i, p in enumerate(personas):
|
272 |
+
c = next(persona_cycler)
|
273 |
+
col = grid_cols[i%2]
|
274 |
+
with col:
|
275 |
+
st.markdown(
|
276 |
+
f"<div style='background:linear-gradient(90deg,{c}18,#181830 95%);"
|
277 |
+
"padding:24px 26px 16px 26px;border-radius:18px;margin-bottom:24px;"
|
278 |
+
f"box-shadow:0 2px 22px {c}22;'>"
|
279 |
+
f"<h2 style='color:{c};margin-bottom:0.18em'>{p['name']}</h2>"
|
280 |
+
f"<div style='color:#fff;font-size:1.15em;font-weight:500;margin-bottom:10px'>Summary: {p['summary']}</div>"
|
281 |
+
f"<div style='color:{neon_pink};font-weight:700;font-size:1.08em;margin-bottom:2px'>Characteristics</div>"
|
282 |
+
f"<ul style='font-size:1.02em;margin-top:3px'>{''.join([f'<li>{b}</li>' for b in p['bullets']])}</ul>"
|
283 |
+
"</div>", unsafe_allow_html=True
|
284 |
+
)
|
285 |
+
persona_names.append(p["name"])
|
286 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
287 |
+
|
288 |
+
if personas and len(reviews) > 0:
|
289 |
+
# Assign reviews to persona via TF-IDF (fast)
|
290 |
+
persona_for_review = assign_review_to_persona_tfidf(df, personas)
|
291 |
+
df_reviews = df.copy()
|
292 |
+
df_reviews = df_reviews.iloc[:len(persona_for_review)].copy()
|
293 |
+
df_reviews["persona"] = persona_for_review
|
294 |
+
|
295 |
+
# --- Generate all summary stats for new graphs
|
296 |
+
# 1. Persona Review Share
|
297 |
+
persona_counts = df_reviews["persona"].value_counts()
|
298 |
+
# 2. Persona Sentiment
|
299 |
+
avg_sentiment = df_reviews.groupby("persona")["polarity"].mean()
|
300 |
+
# 3. Persona Review Length
|
301 |
+
avg_length = df_reviews.groupby("persona")["review_length"].mean()
|
302 |
+
# 4. Persona Emotion (optional)
|
303 |
+
if "main_emotion" not in df_reviews.columns:
|
304 |
+
df_reviews = emotion_pipeline(df_reviews)
|
305 |
+
emo_dist = df_reviews.groupby("persona")["main_emotion"].value_counts().unstack().fillna(0)
|
306 |
+
|
307 |
+
# --- Row 1: Pie and Sentiment Bar
|
308 |
+
c1, c2 = st.columns(2)
|
309 |
+
with c1:
|
310 |
+
st.markdown("<h3 style='color:#fff;font-size:2rem;font-weight:700;'>Sales/Review Share by Persona</h3>", unsafe_allow_html=True)
|
311 |
+
fig = go.Figure(data=[go.Pie(labels=persona_counts.index, values=persona_counts.values, hole=0.45)])
|
312 |
+
fig.update_traces(textinfo='percent+label')
|
313 |
+
st.plotly_chart(fig, use_container_width=True)
|
314 |
+
st.markdown(block_markdown(
|
315 |
+
groq_bullets_persona("Sales/Review Share by Persona", persona_counts.to_dict()), neon_green
|
316 |
+
), unsafe_allow_html=True)
|
317 |
+
|
318 |
+
with c2:
|
319 |
+
st.markdown("<h3 style='color:#fff;font-size:2rem;font-weight:700;'>Average Sentiment by Persona</h3>", unsafe_allow_html=True)
|
320 |
+
fig2 = go.Figure(data=[go.Bar(x=avg_sentiment.index, y=avg_sentiment.values, marker=dict(color=[neon_green, neon_blue, neon_pink, neon_orange]))])
|
321 |
+
fig2.update_layout(xaxis_title="Persona", yaxis_title="Avg Sentiment", font=dict(size=15))
|
322 |
+
st.plotly_chart(fig2, use_container_width=True)
|
323 |
+
st.markdown(block_markdown(
|
324 |
+
groq_bullets_persona("Average Sentiment by Persona", avg_sentiment.to_dict()), neon_blue
|
325 |
+
), unsafe_allow_html=True)
|
326 |
+
|
327 |
+
# --- Row 2: Review Length and Emotion Distribution
|
328 |
+
c3, c4 = st.columns(2)
|
329 |
+
with c3:
|
330 |
+
st.markdown("<h3 style='color:#fff;font-size:2rem;font-weight:700;'>Persona vs. Review Length Distribution</h3>", unsafe_allow_html=True)
|
331 |
+
fig3 = go.Figure(data=[go.Bar(x=avg_length.index, y=avg_length.values, marker=dict(color=[neon_green, neon_blue, neon_pink, neon_orange]))])
|
332 |
+
fig3.update_layout(xaxis_title="Persona", yaxis_title="Avg Review Length", font=dict(size=15))
|
333 |
+
st.plotly_chart(fig3, use_container_width=True)
|
334 |
+
st.markdown(block_markdown(
|
335 |
+
groq_bullets_persona("Average review length (words) by persona", avg_length.to_dict()), neon_orange
|
336 |
+
), unsafe_allow_html=True)
|
337 |
+
|
338 |
+
with c4:
|
339 |
+
st.markdown("<h3 style='color:#fff;font-size:2rem;font-weight:700;'>Persona vs. Emotion Distribution</h3>", unsafe_allow_html=True)
|
340 |
+
fig4 = go.Figure()
|
341 |
+
for idx, em in enumerate(emo_dist.columns):
|
342 |
+
fig4.add_trace(go.Bar(name=em, x=emo_dist.index, y=emo_dist[em].values))
|
343 |
+
fig4.update_layout(barmode='stack', xaxis_title="Persona", yaxis_title="Emotion Count", font=dict(size=15))
|
344 |
+
st.plotly_chart(fig4, use_container_width=True)
|
345 |
+
st.markdown(block_markdown(
|
346 |
+
groq_bullets_persona("Distribution of primary emotions per persona", emo_dist.to_dict()), neon_pink
|
347 |
+
), unsafe_allow_html=True)
|
348 |
+
|
349 |
+
# --- Persona-wise Highlights, grouped by persona with headings ---
|
350 |
+
st.markdown("<hr><h2 style='color:#fff'>Persona-wise Sentiment Highlights & Recommendations</h2>", unsafe_allow_html=True)
|
351 |
+
persona_grid = st.columns(2)
|
352 |
+
|
353 |
+
for idx, p in enumerate(personas):
|
354 |
+
persona_df = df_reviews[df_reviews["persona"] == p["name"]]
|
355 |
+
top_pos = persona_df[persona_df["polarity"] > 0]["review_text"].head(2).tolist()
|
356 |
+
top_neg = persona_df[persona_df["polarity"] < 0]["review_text"].head(2).tolist()
|
357 |
+
pos_summary = groq_bullets_persona(
|
358 |
+
f"Summarize two main positive sentiment points, with percentage, for persona '{p['name']}'.",
|
359 |
+
" ".join(top_pos)
|
360 |
+
) if top_pos else "No positive reviews."
|
361 |
+
neg_summary = groq_bullets_persona(
|
362 |
+
f"Summarize two main negative sentiment points, with percentage, for persona '{p['name']}'.",
|
363 |
+
" ".join(top_neg)
|
364 |
+
) if top_neg else "No negative reviews."
|
365 |
+
|
366 |
+
rec_prompt = (
|
367 |
+
f"You are a product marketing strategist. "
|
368 |
+
f"Based on the review highlights and persona details for '{p['name']}' "
|
369 |
+
f"(do not repeat the characteristics), write one concise or mention name of user, actionable product or marketing recommendation. Dont put * anywhere "
|
370 |
+
f"for the company to better engage this persona. "
|
371 |
+
f"Focus on practical actions the business can take (such as messaging, offers, features, or campaigns). "
|
372 |
+
f"Reply with 1-2 sentences, avoid restating the persona’s traits."
|
373 |
+
)
|
374 |
+
|
375 |
+
try:
|
376 |
+
rec_out = groq_client.chat.completions.create(
|
377 |
+
model=GROQ_MODEL,
|
378 |
+
messages=[
|
379 |
+
{"role": "system", "content": PRODUCT_CONTEXT},
|
380 |
+
{"role": "user", "content": rec_prompt}
|
381 |
+
],
|
382 |
+
max_tokens=80, temperature=0.5
|
383 |
+
).choices[0].message.content.strip()
|
384 |
+
except:
|
385 |
+
rec_out = "No recommendation available."
|
386 |
+
|
387 |
+
with persona_grid[idx % 2]:
|
388 |
+
st.markdown(
|
389 |
+
f"<div style='margin-bottom:38px;padding:18px 20px 8px 20px;border-radius:18px;"
|
390 |
+
f"background:linear-gradient(90deg,{persona_colors[idx%4]}22,#181830 100%);box-shadow:0 2px 22px {persona_colors[idx%4]}18;'>"
|
391 |
+
f"<h2 style='color:{persona_colors[idx%4]};font-size:1.35em;margin-bottom:0.3em'>{p['name']}</h2>"
|
392 |
+
f"<div style='color:#fff;font-size:1.13em;font-weight:400;margin-bottom:14px;'>{p['summary']}</div>"
|
393 |
+
"<div style='margin-bottom:16px'>"
|
394 |
+
f"<b style='color:{neon_green};font-size:1.1em;'>Top Positive Sentiments:</b><br>{block_markdown(pos_summary, neon_green)}"
|
395 |
+
"</div>"
|
396 |
+
"<div style='margin-bottom:16px'>"
|
397 |
+
f"<b style='color:{neon_pink};font-size:1.1em;'>Top Negative Sentiments:</b><br>{block_markdown(neg_summary, neon_pink)}"
|
398 |
+
"</div>"
|
399 |
+
"<div>"
|
400 |
+
f"<b style='color:{neon_yellow};font-size:1.1em;'>Recommendation:</b><br>{block_markdown(rec_out, neon_yellow)}"
|
401 |
+
"</div>"
|
402 |
+
"</div>", unsafe_allow_html=True
|
403 |
+
)
|
404 |
+
|
405 |
+
|
406 |
+
st.markdown("---")
|
407 |
+
st.markdown(
|
408 |
+
f"<small style='color:{neon_yellow}'>Powered By Bugs Fring</small>",
|
409 |
+
unsafe_allow_html=True
|
410 |
+
)
|
src/pages/prt111.py
ADDED
@@ -0,0 +1,700 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import os
|
4 |
+
from PIL import Image
|
5 |
+
import pytesseract
|
6 |
+
import speech_recognition as sr
|
7 |
+
import re
|
8 |
+
from collections import Counter
|
9 |
+
from wordcloud import WordCloud
|
10 |
+
from transformers import pipeline
|
11 |
+
import plotly.graph_objs as go
|
12 |
+
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
|
13 |
+
from groq import Groq
|
14 |
+
import matplotlib.pyplot as plt
|
15 |
+
import numpy as np
|
16 |
+
from itertools import combinations
|
17 |
+
import networkx as nx
|
18 |
+
from sklearn.manifold import TSNE
|
19 |
+
from dotenv import load_dotenv
|
20 |
+
|
21 |
+
|
22 |
+
load_dotenv() # load .env file
|
23 |
+
|
24 |
+
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
25 |
+
|
26 |
+
# --- CONFIG ---
|
27 |
+
GROQ_MODEL = "llama3-70b-8192"
|
28 |
+
groq_client = Groq(api_key=GROQ_API_KEY)
|
29 |
+
PRODUCT_CONTEXT = (
|
30 |
+
"You are analyzing customer reviews for a chocolate-flavoured whey protein powder. "
|
31 |
+
"The product is aimed at fitness enthusiasts and helps with muscle growth and recovery."
|
32 |
+
)
|
33 |
+
RAW_CSV_PATH = "/Users/kushagraaatre/Downloads/Texpedition/data.csv"
|
34 |
+
REVIEW_FOLDER = "/Users/kushagraaatre/Downloads/Texpedition/review_files"
|
35 |
+
DEFAULT_CSV_PATH = "/Users/kushagraaatre/Downloads/Texpedition/data_with_text.csv"
|
36 |
+
|
37 |
+
# Neon colors for blocks
|
38 |
+
neon_blue = "#00fff7"
|
39 |
+
neon_green = "#7CFC00"
|
40 |
+
neon_pink = "#F72585"
|
41 |
+
neon_yellow = "#FFF600"
|
42 |
+
neon_bg = "#181830"
|
43 |
+
neon_orange = "#FFB347"
|
44 |
+
|
45 |
+
# --- UTILS ---
|
46 |
+
def clean_name(name):
|
47 |
+
return (
|
48 |
+
str(name)
|
49 |
+
.strip()
|
50 |
+
.replace('\ufeff', '')
|
51 |
+
.replace('\n', '')
|
52 |
+
.replace('\r', '')
|
53 |
+
.replace('\t', '')
|
54 |
+
.lower()
|
55 |
+
)
|
56 |
+
|
57 |
+
def extract_review_text(df, review_file_dict):
|
58 |
+
review_texts = []
|
59 |
+
for i, row in df.iterrows():
|
60 |
+
fname = clean_name(row['review_file'])
|
61 |
+
file = review_file_dict.get(fname)
|
62 |
+
text = ""
|
63 |
+
if file is None:
|
64 |
+
text = "(missing file)"
|
65 |
+
elif fname.endswith(".txt"):
|
66 |
+
try:
|
67 |
+
with open(file, "r", encoding="utf-8", errors="ignore") as f:
|
68 |
+
text = f.read().strip()
|
69 |
+
if not text:
|
70 |
+
text = "(text unreadable)"
|
71 |
+
except Exception:
|
72 |
+
text = "(text unreadable)"
|
73 |
+
elif fname.endswith(".png"):
|
74 |
+
try:
|
75 |
+
img = Image.open(file)
|
76 |
+
text = pytesseract.image_to_string(img).strip()
|
77 |
+
if not text:
|
78 |
+
text = "(image unreadable)"
|
79 |
+
except Exception:
|
80 |
+
text = "(image unreadable)"
|
81 |
+
elif fname.endswith(".wav"):
|
82 |
+
r = sr.Recognizer()
|
83 |
+
try:
|
84 |
+
with sr.AudioFile(file) as source:
|
85 |
+
audio = r.record(source)
|
86 |
+
text = r.recognize_google(audio)
|
87 |
+
if not text:
|
88 |
+
text = "(audio unreadable)"
|
89 |
+
except Exception:
|
90 |
+
text = "(audio unreadable)"
|
91 |
+
else:
|
92 |
+
text = "(unsupported file)"
|
93 |
+
review_texts.append(text)
|
94 |
+
return review_texts
|
95 |
+
|
96 |
+
@st.cache_resource(show_spinner=True)
|
97 |
+
def get_sentiment_pipeline():
|
98 |
+
return pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english")
|
99 |
+
|
100 |
+
def hf_sentiment(text):
|
101 |
+
try:
|
102 |
+
result = sentiment_pipeline(text[:512])[0]
|
103 |
+
label = result['label']
|
104 |
+
score = result['score']
|
105 |
+
if score <= 0.6:
|
106 |
+
return ("Neutral", 0.0)
|
107 |
+
if label == "POSITIVE" and score > 0.8:
|
108 |
+
return ("Strongly Positive", score)
|
109 |
+
elif label == "POSITIVE":
|
110 |
+
return ("Positive", score)
|
111 |
+
elif label == "NEGATIVE" and score > 0.8:
|
112 |
+
return ("Strongly Negative", -score)
|
113 |
+
else:
|
114 |
+
return ("Negative", -score)
|
115 |
+
except Exception:
|
116 |
+
return ("Neutral", 0.0)
|
117 |
+
|
118 |
+
def groq_bullets(chart_desc, chart_data_text):
|
119 |
+
user_prompt = (
|
120 |
+
f"Summarize as exactly two bullet points the main insights for a chocolate whey protein product, from this chart: {chart_desc}. "
|
121 |
+
f"Here is the relevant data or result: {chart_data_text}. "
|
122 |
+
"Do not use the words 'says', 'shows', 'suggests', 'tells', 'reveals', 'indicates', or any similar phrases. Just facts."
|
123 |
+
)
|
124 |
+
try:
|
125 |
+
chat_completion = groq_client.chat.completions.create(
|
126 |
+
model=GROQ_MODEL,
|
127 |
+
messages=[
|
128 |
+
{"role": "system", "content": PRODUCT_CONTEXT},
|
129 |
+
{"role": "user", "content": user_prompt}
|
130 |
+
],
|
131 |
+
max_tokens=80,
|
132 |
+
temperature=0.6,
|
133 |
+
)
|
134 |
+
bullets = chat_completion.choices[0].message.content.strip()
|
135 |
+
points = [line for line in bullets.splitlines() if line.strip().startswith(("-", "•"))]
|
136 |
+
points = [pt.strip() for pt in points if pt.strip() and not pt.lower().startswith("summary")]
|
137 |
+
return "\n".join(points[:2]) if len(points) >= 2 else "- " + bullets
|
138 |
+
except Exception:
|
139 |
+
return "- Summary not available.\n- (LLM error)"
|
140 |
+
|
141 |
+
def block_markdown(text, color):
|
142 |
+
text = text.replace('\n', '<br>')
|
143 |
+
return (
|
144 |
+
f'<div style="background:linear-gradient(90deg,{color}22,#181830 90%);'
|
145 |
+
f'padding:16px 22px;border-radius:14px;margin:10px 0 24px 0;'
|
146 |
+
f'font-weight:600;color:#fff;font-size:1.04em;line-height:1.6">'
|
147 |
+
f'{text}</div>'
|
148 |
+
)
|
149 |
+
|
150 |
+
def groq_summary_block(prompt):
|
151 |
+
try:
|
152 |
+
resp = groq_client.chat.completions.create(
|
153 |
+
model=GROQ_MODEL,
|
154 |
+
messages=[
|
155 |
+
{"role": "system", "content": PRODUCT_CONTEXT},
|
156 |
+
{"role": "user", "content": prompt}
|
157 |
+
],
|
158 |
+
max_tokens=100,
|
159 |
+
temperature=0.4,
|
160 |
+
)
|
161 |
+
return resp.choices[0].message.content.strip()
|
162 |
+
except Exception:
|
163 |
+
return "(Summary not available.)"
|
164 |
+
|
165 |
+
def groq_top_sentiments(all_text, pos_or_neg="positive"):
|
166 |
+
prompt = (
|
167 |
+
f"Summarize the top 3 {pos_or_neg} sentiments from these customer reviews about a chocolate whey protein powder. "
|
168 |
+
f"Give each sentiment as a short, specific bullet point (not quotes)."
|
169 |
+
f"Reviews: {all_text[:4000]}"
|
170 |
+
)
|
171 |
+
try:
|
172 |
+
resp = groq_client.chat.completions.create(
|
173 |
+
model=GROQ_MODEL,
|
174 |
+
messages=[
|
175 |
+
{"role": "system", "content": PRODUCT_CONTEXT},
|
176 |
+
{"role": "user", "content": prompt}
|
177 |
+
],
|
178 |
+
max_tokens=80,
|
179 |
+
temperature=0.5,
|
180 |
+
)
|
181 |
+
lines = [line for line in resp.choices[0].message.content.strip().split('\n') if line.strip().startswith(("-", "•"))]
|
182 |
+
return "\n".join(lines[:3])
|
183 |
+
except Exception:
|
184 |
+
return "- Not available.\n- (LLM error)"
|
185 |
+
|
186 |
+
def top_n_reviews(df, sentiment, n=3):
|
187 |
+
if sentiment.lower().startswith("pos"):
|
188 |
+
filt = df["sentiment_label"].str.contains("Positive", case=False)
|
189 |
+
top = df.loc[filt].sort_values("polarity", ascending=False)
|
190 |
+
elif sentiment.lower().startswith("neg"):
|
191 |
+
filt = df["sentiment_label"].str.contains("Negative", case=False)
|
192 |
+
# Filter for .txt reviews only
|
193 |
+
if 'review_file' in df.columns:
|
194 |
+
txt_mask = df["review_file"].astype(str).str.endswith('.txt')
|
195 |
+
top = df.loc[filt & txt_mask].sort_values("polarity")
|
196 |
+
else:
|
197 |
+
top = df.loc[filt].sort_values("polarity")
|
198 |
+
else:
|
199 |
+
return []
|
200 |
+
return top["review_text"].head(n).tolist()
|
201 |
+
|
202 |
+
|
203 |
+
# --- LAYOUT ---
|
204 |
+
st.set_page_config(page_title="🌐 Insight Engine", layout="wide", initial_sidebar_state="collapsed")
|
205 |
+
|
206 |
+
# --- AGE TITLE ---
|
207 |
+
st.markdown(
|
208 |
+
"<h1 style='color:#00fff7;font-size:2.65rem;font-weight:900;letter-spacing:0.01em;margin-bottom:5px;'>🌐 Insight Engine</h1>",
|
209 |
+
unsafe_allow_html=True
|
210 |
+
)
|
211 |
+
|
212 |
+
# --- CHEEKY INTRO (PURPLE, Multimodal, Text to Insights) ---
|
213 |
+
st.markdown("""
|
214 |
+
<div style="font-size:1.22rem; color:#AC7CFF; font-weight:600; margin-top:-12px; margin-bottom:11px; line-height:1.56;">
|
215 |
+
🚀 Welcome to your all-in-one playground for market insight magic—supercharged with <b>multimodal skills</b>!
|
216 |
+
Drop in text, images, or even audio—we'll crunch it all and transform bland data into beautiful, actionable insights.
|
217 |
+
Curious what customers really think? Need to turn a wall of reviews into dazzling graphs, smart summaries, and aha-moments?
|
218 |
+
</div>
|
219 |
+
""", unsafe_allow_html=True)
|
220 |
+
|
221 |
+
# --- EXPLANATION FOR THE DEMO DATASET (YELLOW/ORANGE) ---
|
222 |
+
st.markdown("""
|
223 |
+
<div style="font-size:1.12rem; color:#FFB347; font-weight:700; margin-bottom:14px; line-height:1.49;">
|
224 |
+
For this demo, we’ve loaded up a dataset of chocolate protein powder reviews—so you can see all features in action, no setup needed.
|
225 |
+
But hey, The magic works for everything from cookies to kettlebells.
|
226 |
+
</div>
|
227 |
+
""", unsafe_allow_html=True)
|
228 |
+
|
229 |
+
|
230 |
+
# Add custom CSS for neon buttons
|
231 |
+
st.markdown("""
|
232 |
+
<style>
|
233 |
+
.neon-btn {
|
234 |
+
display:inline-block;
|
235 |
+
font-weight:bold;
|
236 |
+
padding:14px 32px;
|
237 |
+
border:none;
|
238 |
+
border-radius:12px;
|
239 |
+
font-size:1.1em;
|
240 |
+
margin-right:18px;
|
241 |
+
cursor:pointer;
|
242 |
+
box-shadow:0 0 14px #00fff777;
|
243 |
+
color:#222 !important;
|
244 |
+
background:linear-gradient(90deg,#7CFC00,#00fff7);
|
245 |
+
text-decoration:none !important;
|
246 |
+
transition: transform 0.08s;
|
247 |
+
}
|
248 |
+
.neon-btn-pink {
|
249 |
+
background:linear-gradient(90deg,#F72585,#00fff7);
|
250 |
+
color:#fff !important;
|
251 |
+
box-shadow:0 0 14px #F7258577;
|
252 |
+
}
|
253 |
+
.neon-btn:hover {
|
254 |
+
transform:scale(1.04);
|
255 |
+
box-shadow:0 0 24px #00fff799;
|
256 |
+
}
|
257 |
+
.neon-btn-pink:hover {
|
258 |
+
box-shadow:0 0 24px #F7258599;
|
259 |
+
}
|
260 |
+
</style>
|
261 |
+
""", unsafe_allow_html=True)
|
262 |
+
|
263 |
+
# Place the links side by side
|
264 |
+
st.markdown("""
|
265 |
+
<div style="display:flex;gap:2em;">
|
266 |
+
<a href="/persona" class="neon-btn"target="_self">👤 Persona Analysis</a>
|
267 |
+
<a href="/newprod" class="neon-btn neon-btn-pink"target="_self">🚀 New Product Launch</a>
|
268 |
+
</div>
|
269 |
+
<br>
|
270 |
+
""", unsafe_allow_html=True)
|
271 |
+
|
272 |
+
# --- LOAD DATA & PREPROCESS ---
|
273 |
+
csv_path = DEFAULT_CSV_PATH
|
274 |
+
if not os.path.exists(csv_path):
|
275 |
+
st.warning(f"Preprocessed CSV not found at {csv_path}. Starting file extraction & text recognition...")
|
276 |
+
if not os.path.exists(RAW_CSV_PATH):
|
277 |
+
st.error(f"Raw CSV file not found at {RAW_CSV_PATH}")
|
278 |
+
st.stop()
|
279 |
+
df = pd.read_csv(RAW_CSV_PATH)
|
280 |
+
review_file_dict = {}
|
281 |
+
if not os.path.exists(REVIEW_FOLDER):
|
282 |
+
st.error(f"Review folder not found at {REVIEW_FOLDER}")
|
283 |
+
st.stop()
|
284 |
+
for fname in os.listdir(REVIEW_FOLDER):
|
285 |
+
key = clean_name(fname)
|
286 |
+
full_path = os.path.join(REVIEW_FOLDER, fname)
|
287 |
+
if os.path.isfile(full_path):
|
288 |
+
review_file_dict[key] = full_path
|
289 |
+
df["review_text"] = extract_review_text(df, review_file_dict)
|
290 |
+
df.to_csv(csv_path, index=False)
|
291 |
+
st.success("Preprocessing complete! Continuing with analysis...")
|
292 |
+
else:
|
293 |
+
df = pd.read_csv(csv_path)
|
294 |
+
|
295 |
+
df["review_text"] = df["review_text"].fillna("")
|
296 |
+
sentiment_pipeline = get_sentiment_pipeline()
|
297 |
+
|
298 |
+
with st.spinner("Running HuggingFace sentiment analysis on reviews... (first time may take a minute)"):
|
299 |
+
df[["sentiment_label", "polarity"]] = df["review_text"].apply(
|
300 |
+
lambda x: hf_sentiment(x) if x and "unreadable" not in x and "missing" not in x else ("Neutral", 0)
|
301 |
+
).apply(pd.Series)
|
302 |
+
|
303 |
+
df["review_length"] = df["review_text"].apply(lambda x: len(str(x).split()))
|
304 |
+
df_valid = df[
|
305 |
+
~df["review_text"].str.contains("unreadable|missing|unsupported", case=False, na=False)
|
306 |
+
& df["review_text"].str.strip().astype(bool)
|
307 |
+
]
|
308 |
+
all_reviews = " ".join(df_valid["review_text"])
|
309 |
+
|
310 |
+
# ----------------- MAIN GRAPHS (numbered, with summaries in blocks) -------------------
|
311 |
+
|
312 |
+
# --- 1 & 2. Sentiment Distribution + Top Themes ---
|
313 |
+
c1, c2 = st.columns(2)
|
314 |
+
with c1:
|
315 |
+
st.subheader("1. Sentiment Distribution")
|
316 |
+
sentiment_counts = df["sentiment_label"].value_counts()
|
317 |
+
color_dict = {
|
318 |
+
"Strongly Positive": neon_green,
|
319 |
+
"Positive": neon_blue,
|
320 |
+
"Neutral": neon_yellow,
|
321 |
+
"Negative": neon_pink,
|
322 |
+
"Strongly Negative": "#c1121f"
|
323 |
+
}
|
324 |
+
colors = [color_dict.get(lbl, "#a67b5b") for lbl in sentiment_counts.index]
|
325 |
+
fig_pie = go.Figure(data=[go.Pie(
|
326 |
+
labels=sentiment_counts.index,
|
327 |
+
values=sentiment_counts.values,
|
328 |
+
hole=0.4,
|
329 |
+
marker=dict(colors=colors),
|
330 |
+
)])
|
331 |
+
fig_pie.update_traces(textinfo='percent+label')
|
332 |
+
fig_pie.update_layout(showlegend=True, legend=dict(orientation="h"), font=dict(size=16))
|
333 |
+
st.plotly_chart(fig_pie, use_container_width=True)
|
334 |
+
st.markdown(block_markdown(groq_bullets("Sentiment distribution pie chart", f"Counts: {sentiment_counts.to_dict()}"), neon_blue), unsafe_allow_html=True)
|
335 |
+
|
336 |
+
with c2:
|
337 |
+
st.subheader("2. Top Themes")
|
338 |
+
if len(df_valid) > 0:
|
339 |
+
vectorizer = TfidfVectorizer(stop_words="english", max_features=10)
|
340 |
+
X = vectorizer.fit_transform(df_valid["review_text"].fillna(""))
|
341 |
+
keywords = [w for w in vectorizer.get_feature_names_out() if len(w) > 2 and w.lower() not in ["says", "tells", "said", "like", "really"]]
|
342 |
+
counts = X.sum(axis=0).A1
|
343 |
+
theme_counts = sorted(zip(keywords, counts), key=lambda x: -x[1])
|
344 |
+
fig_theme = go.Figure(data=[
|
345 |
+
go.Bar(
|
346 |
+
x=[k for k, _ in theme_counts], y=[int(c) for _, c in theme_counts],
|
347 |
+
marker=dict(color=[neon_green, neon_pink, neon_blue, neon_yellow, neon_orange]*2)
|
348 |
+
)
|
349 |
+
])
|
350 |
+
fig_theme.update_layout(xaxis_title='Theme/Keyword', yaxis_title='Frequency', font=dict(size=16))
|
351 |
+
st.plotly_chart(fig_theme, use_container_width=True)
|
352 |
+
st.markdown(block_markdown(
|
353 |
+
groq_bullets("Bar chart of frequency of top review themes",
|
354 |
+
', '.join([k for k,_ in theme_counts])), neon_orange), unsafe_allow_html=True)
|
355 |
+
else:
|
356 |
+
st.write("No valid reviews for theme extraction.")
|
357 |
+
st.markdown(block_markdown("- No data.\n- No chart.", neon_orange), unsafe_allow_html=True)
|
358 |
+
|
359 |
+
st.markdown("---")
|
360 |
+
|
361 |
+
# --- 3 & 4. Sentiment Trend Over Time + Aspect-Based Sentiment ---
|
362 |
+
c3, c4 = st.columns(2)
|
363 |
+
with c3:
|
364 |
+
st.subheader("3. Sentiment Trend Over Time")
|
365 |
+
df_valid = df_valid.reset_index()
|
366 |
+
df_valid["review_idx"] = df_valid.index + 1
|
367 |
+
df_valid_trend = df_valid.groupby("review_idx").agg({"polarity": "mean"}).reset_index()
|
368 |
+
fig_line = go.Figure(data=[
|
369 |
+
go.Scatter(
|
370 |
+
x=df_valid_trend["review_idx"], y=df_valid_trend["polarity"],
|
371 |
+
mode="lines+markers+text",
|
372 |
+
line=dict(color=neon_pink, width=4, dash='dash'),
|
373 |
+
marker=dict(size=8, color=neon_green, symbol="diamond"),
|
374 |
+
)
|
375 |
+
])
|
376 |
+
fig_line.update_layout(
|
377 |
+
xaxis_title="Review Index (chronological)",
|
378 |
+
yaxis_title="Avg Sentiment",
|
379 |
+
font=dict(size=16, color=neon_pink),
|
380 |
+
plot_bgcolor=neon_bg
|
381 |
+
)
|
382 |
+
st.plotly_chart(fig_line, use_container_width=True)
|
383 |
+
st.markdown(block_markdown(
|
384 |
+
groq_bullets("Sentiment trend line over time (reviews in chronological order)",
|
385 |
+
f"Polarity: {list(df_valid_trend['polarity'][:30])}"), neon_pink), unsafe_allow_html=True)
|
386 |
+
|
387 |
+
with c4:
|
388 |
+
st.subheader("4. Aspect-Based Sentiment")
|
389 |
+
aspects = ["price", "quality", "delivery", "taste", "mixability"]
|
390 |
+
aspect_scores = []
|
391 |
+
for aspect in aspects:
|
392 |
+
mask = df_valid["review_text"].str.contains(aspect, case=False, na=False)
|
393 |
+
pols = df_valid.loc[mask, "polarity"]
|
394 |
+
aspect_scores.append(pols.mean() if not pols.empty else 0)
|
395 |
+
fig_aspect = go.Figure(data=[
|
396 |
+
go.Bar(
|
397 |
+
x=aspects, y=aspect_scores,
|
398 |
+
marker=dict(color=[neon_blue, neon_green, neon_pink, neon_yellow, neon_orange])
|
399 |
+
)
|
400 |
+
])
|
401 |
+
fig_aspect.update_layout(xaxis_title="Aspect", yaxis_title="Avg Sentiment", font=dict(size=16))
|
402 |
+
st.plotly_chart(fig_aspect, use_container_width=True)
|
403 |
+
st.markdown(block_markdown(
|
404 |
+
groq_bullets(
|
405 |
+
"Bar chart of sentiment for product aspects (price, quality, delivery, taste, mixability)",
|
406 |
+
str(dict(zip(aspects, [f"{x:.2f}" for x in aspect_scores])))
|
407 |
+
), neon_green), unsafe_allow_html=True)
|
408 |
+
|
409 |
+
|
410 |
+
st.markdown("---")
|
411 |
+
|
412 |
+
# --- 5 & 6. Word Cloud + Review Length Trend ---
|
413 |
+
# Add this above your word cloud and co-occurrence logic
|
414 |
+
stopwords = set("""
|
415 |
+
the and for with you that this are have from all has can will just get out too its on an is in it of to a i my says said tell tells also would could should not as if be do does did was were been being by he she they them their our we us his her its so or at more most some such only may might like one two first second every much well still own even many go goes gone didn't don't isn't aren't wasn't weren't doesn't haven't hadn't can't won't won't wouldn't mustn't protein powder review
|
416 |
+
""".split())
|
417 |
+
|
418 |
+
def filter_tokens(words):
|
419 |
+
return [w for w in words if w not in stopwords and len(w) > 2 and not w.isnumeric()]
|
420 |
+
c5, c6 = st.columns(2)
|
421 |
+
with c5:
|
422 |
+
st.subheader("5. Word Cloud")
|
423 |
+
if all_reviews.strip():
|
424 |
+
words = re.findall(r'\w+', all_reviews.lower())
|
425 |
+
filtered_words = filter_tokens(words)
|
426 |
+
filtered_text = " ".join(filtered_words)
|
427 |
+
wc = WordCloud(
|
428 |
+
width=900, height=400, background_color=neon_bg, colormap='winter',
|
429 |
+
max_words=80, random_state=42
|
430 |
+
).generate(filtered_text)
|
431 |
+
st.image(wc.to_array(), use_column_width=True)
|
432 |
+
top_words = ", ".join([w for w, _ in Counter(filtered_words).most_common(12)])
|
433 |
+
st.markdown(block_markdown(groq_bullets("Word cloud of frequent review words", top_words), neon_yellow), unsafe_allow_html=True)
|
434 |
+
else:
|
435 |
+
st.write("No review text available.")
|
436 |
+
st.markdown(block_markdown("- No text for word cloud.", neon_yellow), unsafe_allow_html=True)
|
437 |
+
|
438 |
+
with c6:
|
439 |
+
st.subheader("6. Review Length Trend")
|
440 |
+
if len(df_valid) > 0:
|
441 |
+
review_lengths = df_valid["review_length"].reset_index(drop=True)
|
442 |
+
fig_line_length = go.Figure(data=[
|
443 |
+
go.Scatter(
|
444 |
+
x=review_lengths.index + 1, y=review_lengths,
|
445 |
+
mode="lines+markers",
|
446 |
+
line=dict(color=neon_orange, width=3)
|
447 |
+
)
|
448 |
+
])
|
449 |
+
fig_line_length.update_layout(
|
450 |
+
xaxis_title="Review (chronological order)",
|
451 |
+
yaxis_title="Review Length (words)",
|
452 |
+
font=dict(size=16), plot_bgcolor=neon_bg
|
453 |
+
)
|
454 |
+
st.plotly_chart(fig_line_length, use_container_width=True)
|
455 |
+
st.markdown(block_markdown(
|
456 |
+
groq_bullets("Line chart showing trend of review lengths (number of words) in chronological order",
|
457 |
+
f"Lengths: {list(review_lengths[:50])}"), neon_orange), unsafe_allow_html=True)
|
458 |
+
else:
|
459 |
+
st.write("No valid reviews for length trend.")
|
460 |
+
st.markdown(block_markdown("- No data.\n- No chart.", neon_orange), unsafe_allow_html=True)
|
461 |
+
|
462 |
+
st.markdown("---")
|
463 |
+
|
464 |
+
# --- 7 & 8. Sentiment Polarity Histogram + Emotion Analysis ---
|
465 |
+
c7, c8 = st.columns(2)
|
466 |
+
with c7:
|
467 |
+
st.subheader("7. Sentiment Polarity Histogram")
|
468 |
+
# Make histogram visually full by using kde line (density)
|
469 |
+
polarity_values = df_valid["polarity"].values
|
470 |
+
fig_hist, ax = plt.subplots(figsize=(7,3))
|
471 |
+
ax.hist(polarity_values, bins=8, color=neon_blue, alpha=0.88, edgecolor="#222", density=True)
|
472 |
+
ax.set_xlabel("Sentiment Polarity Score")
|
473 |
+
ax.set_ylabel("Density")
|
474 |
+
ax.set_title("Distribution of Sentiment Scores")
|
475 |
+
# KDE line
|
476 |
+
if len(polarity_values) > 1:
|
477 |
+
from scipy.stats import gaussian_kde
|
478 |
+
kde = gaussian_kde(polarity_values)
|
479 |
+
x_range = np.linspace(-1, 1, 200)
|
480 |
+
ax.plot(x_range, kde(x_range), color=neon_green, lw=2)
|
481 |
+
st.pyplot(fig_hist)
|
482 |
+
st.markdown(block_markdown(
|
483 |
+
groq_bullets("Histogram of sentiment scores", list(polarity_values[:50])), neon_blue
|
484 |
+
), unsafe_allow_html=True)
|
485 |
+
|
486 |
+
with c8:
|
487 |
+
st.subheader("8. Emotion Analysis Bar Chart")
|
488 |
+
@st.cache_resource(show_spinner=True)
|
489 |
+
def get_emotion_pipeline():
|
490 |
+
return pipeline("text-classification", model="j-hartmann/emotion-english-distilroberta-base", top_k=None)
|
491 |
+
emotion_pipeline = get_emotion_pipeline()
|
492 |
+
emotion_counts = {}
|
493 |
+
for review in df_valid["review_text"]:
|
494 |
+
try:
|
495 |
+
emotions = emotion_pipeline(review[:512])
|
496 |
+
for e in emotions:
|
497 |
+
for d in e:
|
498 |
+
emotion = d['label']
|
499 |
+
if d['score'] > 0.2:
|
500 |
+
emotion_counts[emotion] = emotion_counts.get(emotion, 0) + 1
|
501 |
+
except Exception:
|
502 |
+
continue
|
503 |
+
if emotion_counts:
|
504 |
+
fig_emotion = go.Figure(data=[
|
505 |
+
go.Bar(
|
506 |
+
x=list(emotion_counts.keys()),
|
507 |
+
y=list(emotion_counts.values()),
|
508 |
+
marker=dict(color=[neon_pink, neon_green, neon_blue, neon_yellow, neon_orange])
|
509 |
+
)
|
510 |
+
])
|
511 |
+
fig_emotion.update_layout(xaxis_title="Emotion", yaxis_title="Count", font=dict(size=16))
|
512 |
+
st.plotly_chart(fig_emotion, use_container_width=True)
|
513 |
+
st.markdown(block_markdown(
|
514 |
+
groq_bullets("Bar chart of detected emotions in reviews", str(emotion_counts)), neon_pink
|
515 |
+
), unsafe_allow_html=True)
|
516 |
+
else:
|
517 |
+
st.write("No emotion results (try more reviews).")
|
518 |
+
|
519 |
+
|
520 |
+
st.markdown("---")
|
521 |
+
|
522 |
+
# --- 9 & 10. Bigram/Trigram Frequency + Co-occurrence Network ---
|
523 |
+
c9, c10 = st.columns(2)
|
524 |
+
with c9:
|
525 |
+
st.subheader("9. Bigram/Trigram Frequency")
|
526 |
+
# Use only meaningful ngrams (exclude numbers, names)
|
527 |
+
corpus = df_valid["review_text"].tolist()
|
528 |
+
vect = CountVectorizer(ngram_range=(2,3), stop_words='english', max_features=20, token_pattern=r'\b[a-zA-Z][a-zA-Z]+\b')
|
529 |
+
X_ngram = vect.fit_transform(corpus)
|
530 |
+
ngram_counts = X_ngram.sum(axis=0).A1
|
531 |
+
ngrams = vect.get_feature_names_out()
|
532 |
+
ngram_freq = sorted(zip(ngrams, ngram_counts), key=lambda x: -x[1])
|
533 |
+
fig_ngram = go.Figure(data=[
|
534 |
+
go.Bar(
|
535 |
+
y=[ng for ng,_ in ngram_freq],
|
536 |
+
x=[int(c) for _,c in ngram_freq],
|
537 |
+
orientation='h',
|
538 |
+
marker=dict(color=neon_blue)
|
539 |
+
)
|
540 |
+
])
|
541 |
+
fig_ngram.update_layout(yaxis_title='Phrase', xaxis_title='Count', font=dict(size=15))
|
542 |
+
st.plotly_chart(fig_ngram, use_container_width=True)
|
543 |
+
st.markdown(block_markdown(
|
544 |
+
groq_bullets("Bar chart of most common bigrams/trigrams", ', '.join([f"{ng}: {c}" for ng,c in ngram_freq])), neon_blue
|
545 |
+
), unsafe_allow_html=True)
|
546 |
+
|
547 |
+
with c10:
|
548 |
+
st.subheader("10. Co-occurrence Network Graph")
|
549 |
+
|
550 |
+
def get_top_cooc_words(texts, top_n=12):
|
551 |
+
words = [filter_tokens(re.findall(r'\w+', t.lower())) for t in texts]
|
552 |
+
all_pairs = []
|
553 |
+
for wlist in words:
|
554 |
+
all_pairs.extend(list(combinations(set(wlist), 2)))
|
555 |
+
counter = Counter(all_pairs)
|
556 |
+
return counter.most_common(top_n)
|
557 |
+
|
558 |
+
top_pairs = get_top_cooc_words(df_valid["review_text"])
|
559 |
+
G = nx.Graph()
|
560 |
+
for (a, b), w in top_pairs:
|
561 |
+
G.add_edge(a, b, weight=w)
|
562 |
+
|
563 |
+
# Use Kamada-Kawai layout for more even node spacing
|
564 |
+
pos = nx.kamada_kawai_layout(G)
|
565 |
+
|
566 |
+
# Adjust node and font size for clarity
|
567 |
+
node_count = G.number_of_nodes()
|
568 |
+
base_node_size = 620 if node_count <= 10 else max(390, 1400 // (node_count + 1))
|
569 |
+
font_size = 15 if node_count <= 10 else max(9, 20 - node_count // 2)
|
570 |
+
|
571 |
+
plt.figure(figsize=(7.4, 6.1))
|
572 |
+
nx.draw_networkx_nodes(
|
573 |
+
G, pos, node_color=neon_orange, edgecolors="#fff", linewidths=2,
|
574 |
+
node_size=base_node_size, alpha=0.96
|
575 |
+
)
|
576 |
+
nx.draw_networkx_edges(
|
577 |
+
G, pos,
|
578 |
+
width=[2.2 + G[u][v]['weight'] / 2.4 for u, v in G.edges()],
|
579 |
+
edge_color=neon_blue, alpha=0.76
|
580 |
+
)
|
581 |
+
nx.draw_networkx_labels(
|
582 |
+
G, pos, font_size=font_size, font_color="#212121", font_weight="bold"
|
583 |
+
)
|
584 |
+
plt.axis('off')
|
585 |
+
plt.tight_layout(pad=0.3)
|
586 |
+
st.pyplot(plt.gcf())
|
587 |
+
plt.clf()
|
588 |
+
|
589 |
+
# --- GROQ SUMMARY (2 lines, info box style) ---
|
590 |
+
def groq_summary_graph(prompt):
|
591 |
+
try:
|
592 |
+
resp = groq_client.chat.completions.create(
|
593 |
+
model=GROQ_MODEL,
|
594 |
+
messages=[
|
595 |
+
{"role": "system", "content": PRODUCT_CONTEXT},
|
596 |
+
{"role": "user", "content": prompt}
|
597 |
+
],
|
598 |
+
max_tokens=90,
|
599 |
+
temperature=0.55,
|
600 |
+
)
|
601 |
+
# Remove asterisks, intro, etc
|
602 |
+
lines = [
|
603 |
+
line.strip(" *-•1234567890.").replace("**", "")
|
604 |
+
for line in resp.choices[0].message.content.strip().split("\n")
|
605 |
+
if line.strip()
|
606 |
+
]
|
607 |
+
# Only first 2 lines (you may get 1-3 lines, but only keep 2)
|
608 |
+
return "<br>".join(lines[:2])
|
609 |
+
except Exception:
|
610 |
+
return "Summary not available."
|
611 |
+
|
612 |
+
cooc_pairs_str = "; ".join([f"{a}-{b} ({w})" for (a, b), w in top_pairs])
|
613 |
+
graph_summary = groq_summary_graph(
|
614 |
+
f"Summarize the key relationships or surprising findings in exactly two punchy, non-repetitive lines from this co-occurrence network of customer review words. "
|
615 |
+
f"No generic intro, only crisp insights. Pairs: {cooc_pairs_str}"
|
616 |
+
)
|
617 |
+
|
618 |
+
st.markdown(
|
619 |
+
f"""
|
620 |
+
<div style='background:linear-gradient(90deg,{neon_blue}22,{neon_orange}22);border-radius:14px;padding:18px 22px 12px 22px;margin-top:14px;margin-bottom:14px;box-shadow:0 2px 18px {neon_blue}19;'>
|
621 |
+
<span style='color:{neon_orange};font-size:1.15em;font-weight:800;'>Quick Network Insights:</span><br>
|
622 |
+
<span style='color:#fff;font-size:1.09em;'>{graph_summary}</span>
|
623 |
+
</div>
|
624 |
+
""", unsafe_allow_html=True
|
625 |
+
)
|
626 |
+
|
627 |
+
|
628 |
+
|
629 |
+
|
630 |
+
st.markdown("---")
|
631 |
+
|
632 |
+
# --- 11. Review Cluster Visualization (t-SNE) ---
|
633 |
+
st.subheader("11. Review Cluster Visualization (t-SNE)")
|
634 |
+
vectorizer = TfidfVectorizer(stop_words="english", max_features=100)
|
635 |
+
X = vectorizer.fit_transform(df_valid["review_text"].fillna("")).toarray()
|
636 |
+
tsne = TSNE(n_components=2, random_state=42, perplexity=min(30, max(5, len(df_valid)//2)))
|
637 |
+
X_tsne = tsne.fit_transform(X)
|
638 |
+
fig_tsne = go.Figure(data=[
|
639 |
+
go.Scatter(
|
640 |
+
x=X_tsne[:,0], y=X_tsne[:,1], mode="markers",
|
641 |
+
marker=dict(color=df_valid["polarity"], colorscale="RdYlGn", size=12, showscale=True),
|
642 |
+
text=df_valid["sentiment_label"]
|
643 |
+
)
|
644 |
+
])
|
645 |
+
fig_tsne.update_layout(xaxis_title="t-SNE 1", yaxis_title="t-SNE 2", font=dict(size=16))
|
646 |
+
st.plotly_chart(fig_tsne, use_container_width=True)
|
647 |
+
st.markdown(block_markdown(
|
648 |
+
groq_bullets("2D scatterplot of review clusters by t-SNE", "points colored by sentiment"), neon_blue
|
649 |
+
), unsafe_allow_html=True)
|
650 |
+
|
651 |
+
st.markdown("---")
|
652 |
+
|
653 |
+
# ----------- Final Neon Blocks: Top Quotes and Summaries -----------
|
654 |
+
st.markdown("---")
|
655 |
+
cl1, cl2 = st.columns(2)
|
656 |
+
with cl1:
|
657 |
+
st.markdown(block_markdown(
|
658 |
+
"<b>Top 3 Enthusiastic Positive Reviews:</b><br>" + "<br><br>".join(
|
659 |
+
[f'<span style="color:{neon_green}">“{r}”</span>' for r in top_n_reviews(df_valid, "Positive", 3)]
|
660 |
+
),
|
661 |
+
neon_green), unsafe_allow_html=True)
|
662 |
+
with cl2:
|
663 |
+
st.markdown(block_markdown(
|
664 |
+
"<b>Top 3 Most Critical Negative Reviews:</b><br>" + "<br><br>".join(
|
665 |
+
[f'<span style="color:{neon_pink}">“{r}”</span>' for r in top_n_reviews(df_valid, "Negative", 3)]
|
666 |
+
),
|
667 |
+
neon_pink), unsafe_allow_html=True)
|
668 |
+
|
669 |
+
cl3, cl4 = st.columns(2)
|
670 |
+
with cl3:
|
671 |
+
all_pos_text = " ".join(df_valid[df_valid["polarity"] > 0]["review_text"])
|
672 |
+
st.markdown(block_markdown(
|
673 |
+
"<b>Top 3 Positive Sentiments:</b><br>" + groq_top_sentiments(all_pos_text, "positive"),
|
674 |
+
neon_green), unsafe_allow_html=True)
|
675 |
+
with cl4:
|
676 |
+
all_neg_text = " ".join(df_valid[df_valid["polarity"] < 0]["review_text"])
|
677 |
+
st.markdown(block_markdown(
|
678 |
+
"<b>Top 3 Negative Sentiments:</b><br>" + groq_top_sentiments(all_neg_text, "negative"),
|
679 |
+
neon_pink), unsafe_allow_html=True)
|
680 |
+
|
681 |
+
cl5, cl6 = st.columns(2)
|
682 |
+
with cl5:
|
683 |
+
sentiment_texts = groq_summary_block(
|
684 |
+
"List the top 3 overall customer sentiments about the chocolate whey protein product as short phrases (not sentences, not quotes, just phrases)."
|
685 |
+
)
|
686 |
+
st.markdown(block_markdown(
|
687 |
+
"<b>Top 3 Overall Sentiments:</b><br>" + sentiment_texts.replace('\n', '<br>'),
|
688 |
+
neon_yellow), unsafe_allow_html=True)
|
689 |
+
with cl6:
|
690 |
+
trend_summary = groq_summary_block(
|
691 |
+
"Summarize trends in one short sentence for chocolate protein reviews. "
|
692 |
+
"What do people like most, and what do they dislike most?"
|
693 |
+
)
|
694 |
+
st.markdown(block_markdown(
|
695 |
+
"<b>Summary of Trends:</b><br>" + trend_summary,
|
696 |
+
neon_blue), unsafe_allow_html=True)
|
697 |
+
|
698 |
+
|
699 |
+
|
700 |
+
st.markdown("---\n<small style='color:#7CFC00'>Bugs Fring — End of Report</small>", unsafe_allow_html=True)
|
src/personas.json
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[
|
2 |
+
{
|
3 |
+
"name": "💪 Fitness Enthusiast",
|
4 |
+
"summary": "Avid gym-goers who prioritize performance and recovery.",
|
5 |
+
"bullets": [
|
6 |
+
"Focus on improving stamina, muscle recovery, and post-workout nutrition.",
|
7 |
+
"Value effectiveness, consistency, and ease of use.",
|
8 |
+
"Often mention \"noticeable improvement\" and \"feel more energetic\" in their reviews.",
|
9 |
+
"May be interested in optimizing their workout routine and nutrition plan."
|
10 |
+
]
|
11 |
+
},
|
12 |
+
{
|
13 |
+
"name": "🤔 Practical Shoppers",
|
14 |
+
"summary": "Budget-conscious consumers who weigh price against product quality.",
|
15 |
+
"bullets": [
|
16 |
+
"Frequently mention the product's price, value, and quantity.",
|
17 |
+
"May be willing to compromise on flavor options or texture for a better price.",
|
18 |
+
"Still prioritize effectiveness and ease of use.",
|
19 |
+
"May be interested in finding the best deal for their money."
|
20 |
+
]
|
21 |
+
},
|
22 |
+
{
|
23 |
+
"name": "👅 Flavor Fans",
|
24 |
+
"summary": "Customers who prioritize taste and texture in their supplements.",
|
25 |
+
"bullets": [
|
26 |
+
"Often mention the flavor, consistency, and mixability of the product.",
|
27 |
+
"May be willing to pay a premium for a product that tastes great.",
|
28 |
+
"May be interested in exploring different flavor options or brands.",
|
29 |
+
"May be influenced by reviews that mention taste and texture."
|
30 |
+
]
|
31 |
+
},
|
32 |
+
{
|
33 |
+
"name": "🏋️♂️ Newbies",
|
34 |
+
"summary": "Beginners who are new to the world of fitness and supplements.",
|
35 |
+
"bullets": [
|
36 |
+
"May be enthusiastic and excited about their new fitness journey.",
|
37 |
+
"Often mention being a \"beginner\" or \"new to the gym.\"",
|
38 |
+
"May prioritize ease of use, convenience, and a gentle learning curve.",
|
39 |
+
"May be interested in educational resources or guidance on how to use the product effectively."
|
40 |
+
]
|
41 |
+
}
|
42 |
+
]
|
src/review_files/ 1.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Mary says:
|
2 |
+
Improved my stamina noticeably. I feel more energetic during workouts. Good consistency and not too sweet.
|
src/review_files/ 2.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Christopher says:
|
2 |
+
Great taste and mixes well. The flavor is okay, nothing special.
|
src/review_files/ 3.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Maria says:
|
2 |
+
I feel more energetic during workouts. Noticeable muscle recovery improvement.
|
src/review_files/ 4.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Dawn says:
|
2 |
+
Blends easily with water or milk. Very effective for post-workout nutrition. Slight aftertaste but manageable. Improved my stamina noticeably.
|
src/review_files/ 5.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Amy says:
|
2 |
+
Perfect for daily supplementation. Could have a few more flavor options. No digestive issues so far. Very effective for post-workout nutrition.
|
src/review_files/ 6.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Stephanie says:
|
2 |
+
Bit pricey for the quantity. I feel more energetic during workouts. The flavor is okay, nothing special.
|
src/review_files/ 7.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Janet says:
|
2 |
+
Mild smell, not unpleasant though. Noticeable muscle recovery improvement. Very effective for post-workout nutrition. I feel more energetic during workouts.
|
src/review_files/ 8.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Robin says:
|
2 |
+
Good consistency and not too sweet. Works fine if you’re consistent.
|
src/review_files/ 9.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Cynthia says:
|
2 |
+
Not as filling as expected. Serving scoop could be better marked. Noticeable muscle recovery improvement. Improved my stamina noticeably.
|
src/review_files/.DS_Store
ADDED
Binary file (8.2 kB). View file
|
|
src/review_files/10.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Stanley says:
|
2 |
+
Good consistency and not too sweet. Very effective for post-workout nutrition. Packaging could be more durable.
|
src/review_files/11.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Kyle says:
|
2 |
+
Good consistency and not too sweet. Blends easily with water or milk.
|
src/review_files/12.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Angela says:
|
2 |
+
Bit pricey for the quantity. Good consistency and not too sweet. Texture is decent, not too gritty. Helped me maintain my protein intake.
|
src/review_files/13.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Todd says:
|
2 |
+
Could have a few more flavor options. Perfect for daily supplementation.
|
src/review_files/14.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Thomas says:
|
2 |
+
No digestive issues so far. Bit pricey for the quantity. Could have a few more flavor options.
|
src/review_files/15.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Julia says:
|
2 |
+
Noticeable muscle recovery improvement. Blends easily with water or milk. Blends easily with water or milk. Seems effective but too early to judge.
|
src/review_files/16.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Evelyn says:
|
2 |
+
Perfect for daily supplementation. Perfect for daily supplementation. The flavor is okay, nothing special. Blends easily with water or milk.
|
src/review_files/17.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Bob says:
|
2 |
+
Blends easily with water or milk. Perfect for daily supplementation. Clumps if not shaken properly.
|
src/review_files/18.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Richard says:
|
2 |
+
Great taste and mixes well. Not as filling as expected. Helped me maintain my protein intake. Very effective for post-workout nutrition.
|
src/review_files/19.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Aaron says:
|
2 |
+
Good consistency and not too sweet. Great taste and mixes well.
|
src/review_files/20.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Shelby says:
|
2 |
+
Noticeable muscle recovery improvement. Noticeable muscle recovery improvement.
|
src/review_files/21.png
ADDED
![]() |
src/review_files/22.png
ADDED
![]() |
src/review_files/23.png
ADDED
![]() |
src/review_files/24.png
ADDED
![]() |
src/review_files/25.png
ADDED
![]() |
src/review_files/26.wav
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:e1313f099ee9871d96090cf579fd1dc0d0deffe7ca7aff9e91345b730fd3a79d
|
3 |
+
size 4509774
|
src/review_files/27.wav
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:1349862387c637a1b4b9bf4f3c6d1c882db0566770682edb03276ed916bb0c91
|
3 |
+
size 4655182
|
src/review_files/28.wav
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:6bb2308d55d5d6c13ec0114279a8544c445cca1a3944bef58ded76fb665b3744
|
3 |
+
size 4143182
|
src/review_files/29.wav
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:e1313f099ee9871d96090cf579fd1dc0d0deffe7ca7aff9e91345b730fd3a79d
|
3 |
+
size 4509774
|
src/review_files/30.wav
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:d1debef5f8b69d3b98c85ae545a937b6adfb3571bca6f5e66398970ff97496ba
|
3 |
+
size 5750862
|