File size: 15,414 Bytes
c27f930
 
 
 
 
 
 
 
 
92ca7f3
c18bd07
92ca7f3
 
 
 
 
 
c27f930
 
c18bd07
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92ca7f3
c27f930
 
92ca7f3
 
c27f930
 
 
 
92ca7f3
c27f930
92ca7f3
c27f930
 
 
 
 
 
 
 
 
92ca7f3
c27f930
 
 
 
c18bd07
 
 
 
 
c27f930
 
 
 
 
 
92ca7f3
c27f930
92ca7f3
c18bd07
c27f930
 
c18bd07
 
 
 
 
 
 
 
 
 
 
 
 
92ca7f3
c18bd07
92ca7f3
c18bd07
c27f930
c18bd07
 
c27f930
 
c18bd07
 
 
 
 
 
 
c27f930
c18bd07
 
 
 
 
c27f930
c18bd07
92ca7f3
c18bd07
c27f930
c18bd07
 
c27f930
c18bd07
 
 
 
 
 
 
 
 
 
 
 
 
 
c27f930
c18bd07
 
 
 
c27f930
c18bd07
c27f930
92ca7f3
c27f930
92ca7f3
 
 
 
 
 
 
 
c27f930
92ca7f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c18bd07
 
 
 
 
 
 
 
 
 
92ca7f3
 
c18bd07
92ca7f3
c18bd07
 
 
 
 
 
 
 
 
 
 
 
92ca7f3
 
 
c27f930
92ca7f3
 
c18bd07
c27f930
92ca7f3
 
 
 
 
c27f930
c18bd07
 
 
 
 
 
 
 
 
 
 
c27f930
92ca7f3
 
 
c27f930
 
c18bd07
 
 
 
 
92ca7f3
 
c27f930
 
92ca7f3
 
 
c18bd07
c27f930
92ca7f3
c27f930
 
92ca7f3
c27f930
 
c18bd07
 
 
 
c27f930
 
c18bd07
92ca7f3
c18bd07
 
92ca7f3
c18bd07
92ca7f3
 
 
c18bd07
 
 
 
 
 
 
 
 
 
 
c27f930
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
import streamlit as st
from transformers import (
    pipeline,
    AutoModelForSequenceClassification,
    AutoTokenizer
)
import torch
import re

# ===== CONSTANTS =====
MAX_CHARS = 1500  # Increased character limit
SUPPORTED_LANGUAGES = {
    'en': 'English',
    'zh': 'Chinese',
    'yue': 'Cantonese', 
    'ja': 'Japanese',
    'ko': 'Korean'
}

# ===== ASPECT CONFIGURATION =====
aspect_map = {
    # Location related
    "location": ["location", "near", "close", "access", "transport", "distance", "area", "tsim sha tsui", "kowloon"],
    "view": ["view", "scenery", "vista", "panorama", "outlook", "skyline"],
    "parking": ["parking", "valet", "garage", "car park", "vehicle"],

    # Room related
    "room comfort": ["comfortable", "bed", "pillows", "mattress", "linens", "cozy", "hard", "soft"],
    "room cleanliness": ["clean", "dirty", "spotless", "stains", "hygiene", "sanitation", "dusty"],
    "room amenities": ["amenities", "minibar", "coffee", "tea", "fridge", "facilities", "tv", "kettle"],
    "bathroom": ["bathroom", "shower", "toilet", "sink", "towel", "faucet", "toiletries"],

    # Service related
    "staff service": ["staff", "friendly", "helpful", "rude", "welcoming", "employee", "manager"],
    "reception": ["reception", "check-in", "check-out", "front desk", "welcome", "registration"],
    "housekeeping": ["housekeeping", "maid", "cleaning", "towels", "service", "turndown"],
    "concierge": ["concierge", "recommendation", "advice", "tips", "guidance", "directions"],
    "room service": ["room service", "food delivery", "order", "meal", "tray"],

    # Facilities
    "dining": ["breakfast", "dinner", "restaurant", "meal", "food", "buffet", "lunch"],
    "bar": ["bar", "drinks", "cocktail", "wine", "lounge", "happy hour"],
    "pool": ["pool", "swimming", "jacuzzi", "sun lounger", "deck", "towels"],
    "spa": ["spa", "massage", "treatment", "relax", "wellness", "sauna"],
    "fitness": ["gym", "fitness", "exercise", "workout", "training", "weights"],

    # Technical
    "Wi-Fi": ["wifi", "internet", "connection", "online", "network", "speed"],
    "AC": ["air conditioning", "AC", "temperature", "heating", "cooling", "ventilation"],
    "elevator": ["elevator", "lift", "escalator", "vertical transport", "wait"],

    # Value
    "pricing": ["price", "expensive", "cheap", "value", "rate", "cost", "worth"],
    "extra charges": ["charge", "fee", "bill", "surcharge", "additional", "hidden"]
}

aspect_responses = {
    "location": "We're delighted you enjoyed our prime location in the heart of Tsim Sha Tsui, with convenient access to Nathan Road shopping and the Star Ferry pier.",
    "view": "It's wonderful to hear you appreciated the beautiful harbor or city skyline views from your room.",
    "room comfort": "Our housekeeping team takes special care with our pillow menu and mattress toppers to ensure your comfort.",
    "room cleanliness": "Your commendation of our cleanliness standards means a lot to our dedicated housekeeping staff.",
    "staff service": "Your kind words about our team, especially {staff_name}, have been shared with them - such recognition means everything to us.",
    "reception": "We're pleased our front desk team made your arrival and departure experience seamless.",
    "spa": "Our award-winning spa therapists will be delighted you enjoyed their signature treatments.",
    "pool": "We're glad you had a refreshing time at our rooftop pool with its stunning city views.",
    "dining": "Thank you for appreciating our culinary offerings at The Burgeroom and Chinese Restaurant - we've shared your feedback with Executive Chef Wong.",
    "concierge": "We're happy our concierge team could enhance your stay with their local expertise and recommendations.",
    "fitness": "It's great to hear you made use of our 24-hour fitness center with its panoramic views.",
    "room service": "We're pleased our 24-hour in-room dining met your expectations for both quality and timeliness.",
    "parking": "We're glad our convenient valet parking service made your arrival experience hassle-free.",
    "bathroom": "Our housekeeping team takes special pride in maintaining our marble bathrooms with premium amenities."
}

improvement_actions = {
    "AC": "completed a comprehensive inspection and maintenance of all air conditioning units",
    "housekeeping": "implemented additional training for our housekeeping team and revised cleaning schedules",
    "bathroom": "conducted deep cleaning of all bathrooms and replenished premium toiletries",
    "parking": "introduced new digital key management with our valet service to reduce wait times",
    "dining": "reviewed all menu pricing and quality standards with our culinary leadership team",
    "reception": "provided enhanced customer service training focused on cultural sensitivity",
    "elevator": "performed full servicing of all elevators and adjusted peak-time scheduling",
    "room amenities": "begun upgrading in-room amenities including new coffee machines and smart TVs",
    "Wi-Fi": "upgraded our network infrastructure to provide faster and more reliable internet",
    "noise": "initiated soundproofing improvements in corridors and between rooms",
    "pricing": "started a comprehensive review of our pricing structure and value proposition",
    "room service": "revised our in-room dining operations to improve delivery times",
    "view": "scheduled window cleaning and tree trimming to maintain optimal views",
    "fitness": "upgraded gym equipment based on guest feedback about variety"
}

# ===== MODEL LOADING =====
@st.cache_resource
def load_sentiment_model():
    model = AutoModelForSequenceClassification.from_pretrained("smtsead/fine_tuned_bertweet_hotel")
    tokenizer = AutoTokenizer.from_pretrained('finiteautomata/bertweet-base-sentiment-analysis')
    return model, tokenizer

@st.cache_resource
def load_aspect_classifier():
    return pipeline("zero-shot-classification", model="MoritzLaurer/deberta-v3-base-zeroshot-v1.1-all-33")

# ===== CORE FUNCTIONS =====
def analyze_sentiment(text, model, tokenizer):
    inputs = tokenizer(text, padding=True, truncation=True, max_length=512, return_tensors='pt')
    with torch.no_grad():
        outputs = model(**inputs)
        probs = torch.nn.functional.softmax(outputs.logits, dim=-1)
        predicted_label = torch.argmax(probs).item()
        confidence = torch.max(probs).item()
    return {
        'label': predicted_label,
        'confidence': f"{confidence:.0%}",
        'sentiment': 'POSITIVE' if predicted_label else 'NEGATIVE'
    }

def detect_aspects(text, aspect_classifier):
    relevant_aspects = []
    text_lower = text.lower()
    for aspect, keywords in aspect_map.items():
        if any(re.search(rf'\b{kw}\b', text_lower) for kw in keywords):
            relevant_aspects.append(aspect)
    
    if relevant_aspects:
        result = aspect_classifier(
            text,
            candidate_labels=relevant_aspects,
            multi_label=True,
            hypothesis_template="This review discusses the hotel's {}."
        )
        return [(aspect, f"{score:.0%}") for aspect, score in 
                zip(result['labels'], result['scores']) if score > 0.6]
    return []

def generate_response(sentiment, aspects, original_text):
    # Personalization
    guest_name = ""
    name_match = re.search(r"(Mr\.|Ms\.|Mrs\.)\s(\w+)", original_text, re.IGNORECASE)
    if name_match:
        guest_name = f" {name_match.group(2)}"
    
    # Staff name extraction
    staff_name = ""
    staff_match = re.search(r"(receptionist|manager|concierge|chef)\s(\w+)", original_text, re.IGNORECASE)
    if staff_match:
        staff_name = staff_match.group(2)

    if sentiment['label'] == 1:
        response = f"""Dear{guest_name if guest_name else ' Valued Guest'},

Thank you for choosing The Kimberley Hotel Hong Kong and for sharing your wonderful feedback!"""
        
        # Add relevant aspect responses
        added_aspects = set()
        for aspect, _ in aspects:
            if aspect in aspect_responses:
                response_text = aspect_responses[aspect]
                if "{staff_name}" in response_text and staff_name:
                    response_text = response_text.format(staff_name=staff_name)
                response += "\n\n" + response_text
                added_aspects.add(aspect)
                if len(added_aspects) >= 3:  # Limit to 3 main points
                    break
        
        # Special offers
        if "room" in added_aspects or "dining" in added_aspects:
            response += "\n\nAs a token of our appreciation, we'd like to offer you a complimentary room upgrade or dining credit on your next stay. Simply mention code VIP2024 when booking."
        
        response += "\n\nWe look forward to welcoming you back to your home in Hong Kong!\n\nWarm regards,"
    else:
        response = f"""Dear{guest_name if guest_name else ' Guest'},

Thank you for your valuable feedback - we sincerely apologize that your experience didn't meet our usual high standards."""
        
        # Add improvement actions
        added_improvements = set()
        for aspect, _ in aspects:
            if aspect in improvement_actions:
                response += f"\n\nRegarding your comments about the {aspect}, we've {improvement_actions[aspect]}."
                added_improvements.add(aspect)
                if len(added_improvements) >= 2:  # Limit to 2 main improvements
                    break
        
        # Recovery offer
        recovery_offer = "\n\nTo make amends, we'd like to offer you:"
        if "room" in added_improvements:
            recovery_offer += "\n- One night complimentary room upgrade"
        if "dining" in added_improvements:
            recovery_offer += "\n- HKD 300 dining credit at our restaurants"
        if not ("room" in added_improvements or "dining" in added_improvements):
            recovery_offer += "\n- 15% discount on your next stay"
        
        response += recovery_offer
        response += "\n\nPlease contact our Guest Relations Manager Ms. Chan directly at [email protected] to arrange this."
        
        response += "\n\nWe hope for another opportunity to provide you with the exceptional experience we're known for.\n\nSincerely,"
    
    return response + "\nMichael Wong\nGuest Experience Manager\nThe Kimberley Hotel Hong Kong\n+852 1234 5678"

# ===== STREAMLIT UI =====
def main():
    # Page Config
    st.set_page_config(
        page_title="Kimberley Review Assistant",
        page_icon="🏨",
        layout="centered"
    )
    
    # Custom CSS
    st.markdown("""
    <style>
        .header {
            color: #003366;
            font-size: 28px;
            font-weight: bold;
            margin-bottom: 10px;
        }
        .subheader {
            color: #666666;
            font-size: 16px;
            margin-bottom: 30px;
        }
        .badge {
            background-color: #e6f2ff;
            color: #003366;
            padding: 3px 10px;
            border-radius: 15px;
            font-size: 14px;
            display: inline-block;
            margin: 0 5px 5px 0;
        }
        .char-counter {
            font-size: 12px;
            color: #666;
            text-align: right;
            margin-top: -15px;
            margin-bottom: 15px;
        }
        .char-counter.warning {
            color: #ff6b6b;
        }
        .result-box {
            border-left: 4px solid #003366;
            padding: 15px;
            background-color: #f9f9f9;
            margin: 20px 0;
            border-radius: 0 8px 8px 0;
            white-space: pre-wrap;
        }
        .aspect-badge {
            background-color: #e6f2ff;
            color: #003366;
            padding: 2px 8px;
            border-radius: 4px;
            font-size: 14px;
            display: inline-block;
            margin: 2px;
        }
    </style>
    """, unsafe_allow_html=True)
    
    # Header
    st.markdown('<div class="header">The Kimberley Hotel Hong Kong</div>', unsafe_allow_html=True)
    st.markdown('<div class="subheader">Guest Review Analysis System</div>', unsafe_allow_html=True)
    
    # Supported Languages
    st.markdown("**Supported Review Languages:**")
    lang_cols = st.columns(5)
    for i, (code, name) in enumerate(SUPPORTED_LANGUAGES.items()):
        lang_cols[i].markdown(f'<div class="badge">{name}</div>', unsafe_allow_html=True)
    
    # Review Input with Character Counter
    review = st.text_area("**Paste Guest Review:**", 
                         height=250,
                         max_chars=MAX_CHARS,
                         placeholder=f"Enter review in any supported language (max {MAX_CHARS} characters)...",
                         key="review_input")
    
    char_count = len(st.session_state.review_input) if 'review_input' in st.session_state else 0
    char_class = "warning" if char_count > MAX_CHARS else ""
    st.markdown(f'<div class="char-counter {char_class}">{char_count}/{MAX_CHARS} characters</div>', 
                unsafe_allow_html=True)
    
    if st.button("Analyze & Generate Response", type="primary"):
        if not review.strip():
            st.error("Please enter a review")
            return
        
        if char_count > MAX_CHARS:
            st.warning(f"Review truncated to {MAX_CHARS} characters for analysis")
            review = review[:MAX_CHARS]
        
        with st.spinner("Analyzing feedback..."):
            # Load models
            sentiment_model, tokenizer = load_sentiment_model()
            aspect_classifier = load_aspect_classifier()
            
            # Process review
            sentiment = analyze_sentiment(review, sentiment_model, tokenizer)
            aspects = detect_aspects(review, aspect_classifier)
            response = generate_response(sentiment, aspects, review)
            
            # Display results
            st.divider()
            
            # Sentiment and Aspects
            col1, col2 = st.columns(2)
            with col1:
                st.markdown("### Sentiment Analysis")
                sentiment_icon = "✅" if sentiment['label'] == 1 else "⚠️"
                st.markdown(f"{sentiment_icon} **{sentiment['sentiment']}**")
                st.caption(f"Confidence level: {sentiment['confidence']}")
            
            with col2:
                st.markdown("### Key Aspects Detected")
                if aspects:
                    for aspect, score in sorted(aspects, key=lambda x: float(x[1][:-1]), reverse=True):
                        st.markdown(f'<div class="aspect-badge">{aspect} ({score})</div>', unsafe_allow_html=True)
                else:
                    st.markdown("_No specific aspects detected_")
            
            # Generated Response
            st.divider()
            st.markdown("### Draft Response")
            st.markdown(f'<div class="result-box">{response}</div>', unsafe_allow_html=True)
            
            # Copy button
            if st.button("Copy Response to Clipboard"):
                st.session_state.copied = True
                st.rerun()
            
            if st.session_state.get("copied", False):
                st.success("Response copied to clipboard!")
                st.session_state.copied = False

if __name__ == "__main__":
    main()