Skip to content

Commit d008637

Browse files
committed
Add responsive UI design and product image proxy
- Implement responsive design across all UI components using CSS clamp() - Add breakpoints for mobile (576px), tablet (768px, 992px), and desktop (1200px, 1400px) - Update WelcomeCard, ChatPanel, ChatHistory, InlineProductSelector, InlineContentPreview - Add /api/product-images/<filename> proxy endpoint for private storage access - Update /api/products endpoints to return proxy URLs instead of direct blob URLs - Add DNS update script for backend container IP changes - Update .gitignore to exclude build artifacts (static/, node_modules/, zip files)
1 parent d7a93af commit d008637

13 files changed

Lines changed: 491 additions & 251 deletions

File tree

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,14 @@ eggs/
3737

3838
# Node
3939
/content-gen/src/frontend/node_modules/
40+
/content-gen/src/frontend-server/node_modules/
41+
/content-gen/src/frontend-server/static/
42+
/content-gen/src/frontend-server/*.zip
4043
node_modules/
4144

45+
# Build output
46+
/content-gen/src/static/
47+
4248
# Logs
4349
*.log
4450
logs/
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/bin/bash
2+
# Script to update the backend DNS record after container restart
3+
# This keeps the BACKEND_URL stable (backend.contentgen.internal)
4+
# while updating the underlying IP when the container gets a new one.
5+
6+
set -e
7+
8+
RESOURCE_GROUP="${RESOURCE_GROUP:-rg-contentgen-jahunte}"
9+
CONTAINER_NAME="${CONTAINER_NAME:-aci-contentgen-backend}"
10+
DNS_ZONE="contentgen.internal"
11+
RECORD_NAME="backend"
12+
13+
echo "Fetching current container IP..."
14+
NEW_IP=$(az container show -g "$RESOURCE_GROUP" -n "$CONTAINER_NAME" --query "ipAddress.ip" -o tsv)
15+
echo "Current container IP: $NEW_IP"
16+
17+
echo "Fetching current DNS record IP..."
18+
CURRENT_IP=$(az network private-dns record-set a show -g "$RESOURCE_GROUP" -z "$DNS_ZONE" -n "$RECORD_NAME" --query "aRecords[0].ipv4Address" -o tsv 2>/dev/null || echo "")
19+
20+
if [ "$CURRENT_IP" == "$NEW_IP" ]; then
21+
echo "✓ DNS record is already up to date ($NEW_IP)"
22+
exit 0
23+
fi
24+
25+
if [ -n "$CURRENT_IP" ]; then
26+
echo "Removing old DNS record ($CURRENT_IP)..."
27+
az network private-dns record-set a remove-record \
28+
-g "$RESOURCE_GROUP" \
29+
-z "$DNS_ZONE" \
30+
-n "$RECORD_NAME" \
31+
-a "$CURRENT_IP" \
32+
--keep-empty-record-set
33+
fi
34+
35+
echo "Adding new DNS record ($NEW_IP)..."
36+
az network private-dns record-set a add-record \
37+
-g "$RESOURCE_GROUP" \
38+
-z "$DNS_ZONE" \
39+
-n "$RECORD_NAME" \
40+
-a "$NEW_IP"
41+
42+
echo "✓ DNS record updated: $RECORD_NAME.$DNS_ZONE -> $NEW_IP"
43+
echo ""
44+
echo "The App Service will automatically use the new IP via:"
45+
echo " BACKEND_URL=http://backend.contentgen.internal:8000"

content-gen/src/app.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ async def generate():
418418
)
419419

420420

421-
# ==================== Image Proxy Endpoint ====================
421+
# ==================== Image Proxy Endpoints ====================
422422

423423
@app.route("/api/images/<conversation_id>/<filename>", methods=["GET"])
424424
async def proxy_generated_image(conversation_id: str, filename: str):
@@ -452,6 +452,38 @@ async def proxy_generated_image(conversation_id: str, filename: str):
452452
return jsonify({"error": "Image not found"}), 404
453453

454454

455+
@app.route("/api/product-images/<filename>", methods=["GET"])
456+
async def proxy_product_image(filename: str):
457+
"""
458+
Proxy product images from blob storage.
459+
This allows the frontend to access product images via private endpoint.
460+
The filename should match the blob name (e.g., SnowVeil.png).
461+
"""
462+
try:
463+
blob_service = await get_blob_service()
464+
await blob_service.initialize()
465+
466+
blob_client = blob_service._product_images_container.get_blob_client(filename)
467+
468+
# Download the blob
469+
download = await blob_client.download_blob()
470+
image_data = await download.readall()
471+
472+
# Determine content type from filename
473+
content_type = "image/png" if filename.endswith(".png") else "image/jpeg"
474+
475+
return Response(
476+
image_data,
477+
mimetype=content_type,
478+
headers={
479+
"Cache-Control": "public, max-age=86400", # Cache for 24 hours
480+
}
481+
)
482+
except Exception as e:
483+
logger.exception(f"Error proxying product image {filename}: {e}")
484+
return jsonify({"error": "Image not found"}), 404
485+
486+
455487
# ==================== Product Endpoints ====================
456488

457489
@app.route("/api/products", methods=["GET"])
@@ -481,9 +513,21 @@ async def list_products():
481513
else:
482514
products = await cosmos_service.get_all_products(limit)
483515

516+
# Convert blob URLs to proxy URLs for products with images
517+
product_list = []
518+
for p in products:
519+
product_dict = p.model_dump()
520+
# Convert direct blob URL to proxy URL
521+
if product_dict.get("image_url"):
522+
# Extract filename from URL like https://account.blob.../container/SnowVeil.png
523+
original_url = product_dict["image_url"]
524+
filename = original_url.split("/")[-1] if "/" in original_url else original_url
525+
product_dict["image_url"] = f"/api/product-images/{filename}"
526+
product_list.append(product_dict)
527+
484528
return jsonify({
485-
"products": [p.model_dump() for p in products],
486-
"count": len(products)
529+
"products": product_list,
530+
"count": len(product_list)
487531
})
488532

489533

@@ -496,7 +540,14 @@ async def get_product(sku: str):
496540
if not product:
497541
return jsonify({"error": "Product not found"}), 404
498542

499-
return jsonify(product.model_dump())
543+
product_dict = product.model_dump()
544+
# Convert direct blob URL to proxy URL
545+
if product_dict.get("image_url"):
546+
original_url = product_dict["image_url"]
547+
filename = original_url.split("/")[-1] if "/" in original_url else original_url
548+
product_dict["image_url"] = f"/api/product-images/{filename}"
549+
550+
return jsonify(product_dict)
500551

501552

502553
@app.route("/api/products", methods=["POST"])

content-gen/src/frontend/src/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,13 +374,14 @@ function App() {
374374
display: 'flex',
375375
alignItems: 'center',
376376
justifyContent: 'space-between',
377-
padding: '12px 24px',
377+
padding: 'clamp(8px, 1.5vw, 12px) clamp(16px, 3vw, 24px)',
378378
borderBottom: `1px solid ${tokens.colorNeutralStroke2}`,
379379
backgroundColor: tokens.colorNeutralBackground1,
380+
flexShrink: 0,
380381
}}>
381-
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
382+
<div style={{ display: 'flex', alignItems: 'center', gap: 'clamp(8px, 1.5vw, 10px)' }}>
382383
<ContosoLogo />
383-
<Text weight="semibold" size={500} style={{ color: tokens.colorNeutralForeground1 }}>
384+
<Text weight="semibold" size={500} style={{ color: tokens.colorNeutralForeground1, fontSize: 'clamp(16px, 2.5vw, 20px)' }}>
384385
Contoso
385386
</Text>
386387
</div>

content-gen/src/frontend/src/components/ChatHistory.tsx

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -148,18 +148,26 @@ export function ChatHistory({
148148
height: '100%',
149149
display: 'flex',
150150
flexDirection: 'column',
151-
padding: '16px',
151+
padding: 'clamp(12px, 2vw, 16px)',
152152
backgroundColor: tokens.colorNeutralBackground2,
153153
borderRadius: '8px',
154+
overflow: 'hidden',
154155
}}>
155156
{/* Header */}
156157
<div style={{
157158
display: 'flex',
158159
alignItems: 'center',
159160
justifyContent: 'space-between',
160-
marginBottom: '16px',
161+
marginBottom: 'clamp(12px, 2vw, 16px)',
162+
flexShrink: 0,
161163
}}>
162-
<Text weight="semibold" size={400}>Chat History</Text>
164+
<Text
165+
weight="semibold"
166+
size={400}
167+
style={{ fontSize: 'clamp(14px, 2vw, 16px)' }}
168+
>
169+
Chat History
170+
</Text>
163171
</div>
164172

165173
{/* Conversation List */}
@@ -169,20 +177,21 @@ export function ChatHistory({
169177
display: 'flex',
170178
flexDirection: 'column',
171179
gap: '4px',
180+
minHeight: 0, /* Allow flex shrinking */
172181
}}>
173182
{isLoading ? (
174183
<div style={{
175184
display: 'flex',
176185
justifyContent: 'center',
177186
alignItems: 'center',
178-
padding: '32px'
187+
padding: 'clamp(16px, 4vw, 32px)'
179188
}}>
180189
<Spinner size="small" label="Loading..." />
181190
</div>
182191
) : error ? (
183192
<div style={{
184193
textAlign: 'center',
185-
padding: '32px',
194+
padding: 'clamp(16px, 4vw, 32px)',
186195
color: tokens.colorNeutralForeground3
187196
}}>
188197
<Text size={200}>{error}</Text>
@@ -196,10 +205,10 @@ export function ChatHistory({
196205
) : displayConversations.length === 0 ? (
197206
<div style={{
198207
textAlign: 'center',
199-
padding: '32px',
208+
padding: 'clamp(16px, 4vw, 32px)',
200209
color: tokens.colorNeutralForeground3
201210
}}>
202-
<Chat24Regular style={{ fontSize: '24px', marginBottom: '8px', opacity: 0.5 }} />
211+
<Chat24Regular style={{ fontSize: 'clamp(20px, 3vw, 24px)', marginBottom: '8px', opacity: 0.5 }} />
203212
<Text size={200} block>No conversations yet</Text>
204213
</div>
205214
) : (
@@ -220,12 +229,13 @@ export function ChatHistory({
220229

221230
{/* Footer Links */}
222231
<div style={{
223-
marginTop: '16px',
224-
paddingTop: '16px',
232+
marginTop: 'clamp(12px, 2vw, 16px)',
233+
paddingTop: 'clamp(12px, 2vw, 16px)',
225234
borderTop: `1px solid ${tokens.colorNeutralStroke2}`,
226235
display: 'flex',
227236
flexDirection: 'column',
228-
gap: '12px',
237+
gap: 'clamp(8px, 1.5vw, 12px)',
238+
flexShrink: 0,
229239
}}>
230240
{hasMore && (
231241
<Link
@@ -234,7 +244,7 @@ export function ChatHistory({
234244
display: 'flex',
235245
alignItems: 'center',
236246
gap: '4px',
237-
fontSize: '13px',
247+
fontSize: 'clamp(11px, 1.5vw, 13px)',
238248
}}
239249
>
240250
{showAll ? 'Show less' : 'See all'}
@@ -251,10 +261,10 @@ export function ChatHistory({
251261
display: 'flex',
252262
alignItems: 'center',
253263
gap: '6px',
254-
fontSize: '13px',
264+
fontSize: 'clamp(11px, 1.5vw, 13px)',
255265
}}
256266
>
257-
<Add24Regular style={{ fontSize: '16px' }} />
267+
<Add24Regular style={{ fontSize: 'clamp(14px, 2vw, 16px)' }} />
258268
Start new chat
259269
</Link>
260270
</div>
@@ -285,7 +295,7 @@ function ConversationItem({
285295
onMouseEnter={() => setIsHovered(true)}
286296
onMouseLeave={() => setIsHovered(false)}
287297
style={{
288-
padding: '10px 12px',
298+
padding: 'clamp(8px, 1.5vw, 10px) clamp(10px, 1.5vw, 12px)',
289299
borderRadius: '6px',
290300
cursor: 'pointer',
291301
backgroundColor: isActive
@@ -309,6 +319,7 @@ function ConversationItem({
309319
textOverflow: 'ellipsis',
310320
whiteSpace: 'nowrap',
311321
display: 'block',
322+
fontSize: 'clamp(11px, 1.5vw, 13px)',
312323
}}
313324
>
314325
{conversation.title || 'Untitled'}
@@ -317,6 +328,7 @@ function ConversationItem({
317328
size={100}
318329
style={{
319330
color: tokens.colorNeutralForeground4,
331+
fontSize: 'clamp(10px, 1.2vw, 12px)',
320332
}}
321333
>
322334
{formatTimestamp(conversation.timestamp)}
@@ -326,7 +338,7 @@ function ConversationItem({
326338
{isHovered && (
327339
<Button
328340
appearance="subtle"
329-
icon={<Delete24Regular style={{ fontSize: '16px' }} />}
341+
icon={<Delete24Regular style={{ fontSize: 'clamp(14px, 2vw, 16px)' }} />}
330342
size="small"
331343
onClick={onDelete}
332344
style={{

0 commit comments

Comments
 (0)