Spaces:
Running
Running
SamiKoen commited on
Commit ·
fb67fbd
1
Parent(s): 24bc478
Add split-screen product display: avatar left, product image right
Browse files- app.py +82 -0
- static/index.html +150 -30
app.py
CHANGED
|
@@ -264,6 +264,74 @@ async def handle_tool_call(name: str, arguments: dict) -> str:
|
|
| 264 |
return f"Hata: {e}"
|
| 265 |
|
| 266 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
@app.websocket("/ws")
|
| 268 |
async def realtime_relay(client_ws: WebSocket):
|
| 269 |
"""Browser <-> OpenAI Realtime API arasinda WebSocket relay."""
|
|
@@ -360,6 +428,20 @@ async def realtime_relay(client_ws: WebSocket):
|
|
| 360 |
logger.info(f"Tool call: {fn_name}({args})")
|
| 361 |
result = await handle_tool_call(fn_name, args)
|
| 362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
# Tool sonucunu OpenAI'ye geri gonder
|
| 364 |
await openai_ws.send(json.dumps({
|
| 365 |
"type": "conversation.item.create",
|
|
|
|
| 264 |
return f"Hata: {e}"
|
| 265 |
|
| 266 |
|
| 267 |
+
def find_product_in_trek_xml(query: str) -> dict | None:
|
| 268 |
+
"""Trek XML'inden urun arar — name, image, link dondurur. Frontend product display icin."""
|
| 269 |
+
try:
|
| 270 |
+
from smart_warehouse_with_price import get_cached_trek_xml
|
| 271 |
+
import re
|
| 272 |
+
|
| 273 |
+
xml = get_cached_trek_xml()
|
| 274 |
+
if not xml:
|
| 275 |
+
return None
|
| 276 |
+
try:
|
| 277 |
+
text = xml.decode('utf-8', errors='replace') if isinstance(xml, bytes) else xml
|
| 278 |
+
except Exception:
|
| 279 |
+
text = str(xml)
|
| 280 |
+
|
| 281 |
+
# Sorguyu normalize et
|
| 282 |
+
tr_map = {'İ': 'i', 'I': 'i', 'ı': 'i', 'Ğ': 'g', 'ğ': 'g',
|
| 283 |
+
'Ü': 'u', 'ü': 'u', 'Ş': 's', 'ş': 's',
|
| 284 |
+
'Ö': 'o', 'ö': 'o', 'Ç': 'c', 'ç': 'c'}
|
| 285 |
+
def normalize(s: str) -> str:
|
| 286 |
+
for tr, en in tr_map.items():
|
| 287 |
+
s = s.replace(tr, en)
|
| 288 |
+
return s.lower()
|
| 289 |
+
|
| 290 |
+
q_norm = normalize(query)
|
| 291 |
+
# Anlamsiz kelimeleri at, anlamli token'lar kalsin (>=3 harf)
|
| 292 |
+
stop = {'var', 'mi', 'fiyat', 'stok', 'kac', 'tl', 'ne', 'bu', 'soyle',
|
| 293 |
+
'bilgi', 'verir', 'verirmisin', 'nedir', 'lutfen', 'tum',
|
| 294 |
+
'gosterir', 'gosterirmisin', 'urun', 'icin'}
|
| 295 |
+
tokens = [t for t in re.findall(r'[a-z0-9+]+', q_norm)
|
| 296 |
+
if len(t) >= 3 and t not in stop]
|
| 297 |
+
if not tokens:
|
| 298 |
+
return None
|
| 299 |
+
|
| 300 |
+
# XML'i item'lara ayir
|
| 301 |
+
items = re.findall(r'<item>(.*?)</item>', text, re.DOTALL)
|
| 302 |
+
best = None
|
| 303 |
+
best_score = 0
|
| 304 |
+
for it in items:
|
| 305 |
+
label_m = re.search(r'<rootlabel><!\[CDATA\[(.*?)\]\]></rootlabel>', it)
|
| 306 |
+
if not label_m:
|
| 307 |
+
continue
|
| 308 |
+
label = label_m.group(1)
|
| 309 |
+
label_norm = normalize(label)
|
| 310 |
+
score = sum(1 for t in tokens if t in label_norm)
|
| 311 |
+
# Variant olmayan ana urunleri tercih et
|
| 312 |
+
is_variant_m = re.search(r'<isOptionOfAProduct>(\d+)</isOptionOfAProduct>', it)
|
| 313 |
+
if is_variant_m and is_variant_m.group(1) == '0':
|
| 314 |
+
score += 0.5
|
| 315 |
+
if score > best_score:
|
| 316 |
+
best_score = score
|
| 317 |
+
best = it
|
| 318 |
+
best_label = label
|
| 319 |
+
|
| 320 |
+
if not best or best_score < 1:
|
| 321 |
+
return None
|
| 322 |
+
|
| 323 |
+
img_m = re.search(r'<picture1Path><!\[CDATA\[(.*?)\]\]></picture1Path>', best)
|
| 324 |
+
link_m = re.search(r'<productLink><!\[CDATA\[(.*?)\]\]></productLink>', best)
|
| 325 |
+
return {
|
| 326 |
+
'name': best_label,
|
| 327 |
+
'image': img_m.group(1) if img_m else None,
|
| 328 |
+
'link': link_m.group(1) if link_m else None,
|
| 329 |
+
}
|
| 330 |
+
except Exception:
|
| 331 |
+
logger.exception('find_product_in_trek_xml hatasi')
|
| 332 |
+
return None
|
| 333 |
+
|
| 334 |
+
|
| 335 |
@app.websocket("/ws")
|
| 336 |
async def realtime_relay(client_ws: WebSocket):
|
| 337 |
"""Browser <-> OpenAI Realtime API arasinda WebSocket relay."""
|
|
|
|
| 428 |
logger.info(f"Tool call: {fn_name}({args})")
|
| 429 |
result = await handle_tool_call(fn_name, args)
|
| 430 |
|
| 431 |
+
# Urun resmi bul ve client'a gonder (split-screen display icin)
|
| 432 |
+
if fn_name == "get_warehouse_stock":
|
| 433 |
+
query = args.get("user_message", "")
|
| 434 |
+
product = find_product_in_trek_xml(query)
|
| 435 |
+
if product and product.get("image"):
|
| 436 |
+
logger.info(f"[product] {product['name']}")
|
| 437 |
+
try:
|
| 438 |
+
await client_ws.send_text(json.dumps({
|
| 439 |
+
"type": "product.show",
|
| 440 |
+
"product": product,
|
| 441 |
+
}))
|
| 442 |
+
except Exception:
|
| 443 |
+
pass
|
| 444 |
+
|
| 445 |
# Tool sonucunu OpenAI'ye geri gonder
|
| 446 |
await openai_ws.send(json.dumps({
|
| 447 |
"type": "conversation.item.create",
|
static/index.html
CHANGED
|
@@ -28,12 +28,105 @@
|
|
| 28 |
linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%);
|
| 29 |
color: #fff;
|
| 30 |
min-height: 100vh;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
display: flex;
|
| 32 |
flex-direction: column;
|
| 33 |
align-items: center;
|
| 34 |
-
padding: 28px
|
| 35 |
-
|
| 36 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
body::before {
|
| 38 |
content: '';
|
| 39 |
position: fixed;
|
|
@@ -206,39 +299,52 @@
|
|
| 206 |
</style>
|
| 207 |
</head>
|
| 208 |
<body>
|
| 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 |
</div>
|
|
|
|
|
|
|
| 239 |
</div>
|
| 240 |
|
| 241 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
<script>
|
| 244 |
const SAMPLE_RATE = 24000;
|
|
@@ -370,8 +476,22 @@ async function connect() {
|
|
| 370 |
ws.onerror = () => setStatus('Bağlantı hatası', 'disconnected');
|
| 371 |
}
|
| 372 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
function handleEvent(evt) {
|
| 374 |
switch (evt.type) {
|
|
|
|
|
|
|
|
|
|
| 375 |
case 'response.audio.delta':
|
| 376 |
case 'response.output_audio.delta':
|
| 377 |
if (evt.delta) playPCM16(base64ToInt16(evt.delta));
|
|
|
|
| 28 |
linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%);
|
| 29 |
color: #fff;
|
| 30 |
min-height: 100vh;
|
| 31 |
+
padding: 0;
|
| 32 |
+
letter-spacing: 0.01em;
|
| 33 |
+
}
|
| 34 |
+
.app {
|
| 35 |
+
display: grid;
|
| 36 |
+
grid-template-columns: 1fr 1fr;
|
| 37 |
+
min-height: 100vh;
|
| 38 |
+
position: relative;
|
| 39 |
+
z-index: 1;
|
| 40 |
+
}
|
| 41 |
+
.col {
|
| 42 |
display: flex;
|
| 43 |
flex-direction: column;
|
| 44 |
align-items: center;
|
| 45 |
+
padding: 28px 24px 36px;
|
| 46 |
+
min-width: 0;
|
| 47 |
}
|
| 48 |
+
.col.left { border-right: 1px solid var(--line); }
|
| 49 |
+
.col.right { justify-content: center; }
|
| 50 |
+
@media (max-width: 900px) {
|
| 51 |
+
.app { grid-template-columns: 1fr; }
|
| 52 |
+
.col.left { border-right: none; border-bottom: 1px solid var(--line); }
|
| 53 |
+
}
|
| 54 |
+
.product-card {
|
| 55 |
+
width: min(90%, 520px);
|
| 56 |
+
background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.015));
|
| 57 |
+
border: 1px solid var(--line);
|
| 58 |
+
border-radius: 4px;
|
| 59 |
+
padding: 28px;
|
| 60 |
+
text-align: center;
|
| 61 |
+
position: relative;
|
| 62 |
+
}
|
| 63 |
+
.product-card::before {
|
| 64 |
+
content: '';
|
| 65 |
+
position: absolute;
|
| 66 |
+
top: 0; left: 18px; right: 18px;
|
| 67 |
+
height: 1px;
|
| 68 |
+
background: linear-gradient(90deg, transparent, var(--trek-red), transparent);
|
| 69 |
+
}
|
| 70 |
+
.product-card .empty {
|
| 71 |
+
font-family: 'Rajdhani', sans-serif;
|
| 72 |
+
font-size: 1rem;
|
| 73 |
+
letter-spacing: 0.2em;
|
| 74 |
+
text-transform: uppercase;
|
| 75 |
+
color: rgba(255,255,255,0.35);
|
| 76 |
+
padding: 80px 0;
|
| 77 |
+
}
|
| 78 |
+
.product-card .empty .ico {
|
| 79 |
+
display: block;
|
| 80 |
+
font-size: 3rem;
|
| 81 |
+
margin-bottom: 18px;
|
| 82 |
+
color: rgba(226,35,26,0.5);
|
| 83 |
+
}
|
| 84 |
+
.product-card .pimg {
|
| 85 |
+
width: 100%;
|
| 86 |
+
aspect-ratio: 1 / 1;
|
| 87 |
+
object-fit: contain;
|
| 88 |
+
background: #fff;
|
| 89 |
+
border-radius: 2px;
|
| 90 |
+
margin-bottom: 20px;
|
| 91 |
+
border: 1px solid var(--line);
|
| 92 |
+
}
|
| 93 |
+
.product-card .pname {
|
| 94 |
+
font-family: 'Rajdhani', sans-serif;
|
| 95 |
+
font-size: 1.4rem;
|
| 96 |
+
font-weight: 700;
|
| 97 |
+
letter-spacing: 0.04em;
|
| 98 |
+
text-transform: uppercase;
|
| 99 |
+
margin-bottom: 14px;
|
| 100 |
+
line-height: 1.2;
|
| 101 |
+
}
|
| 102 |
+
.product-card .plink {
|
| 103 |
+
display: inline-block;
|
| 104 |
+
padding: 10px 22px;
|
| 105 |
+
border: 1px solid var(--trek-red);
|
| 106 |
+
color: var(--trek-red);
|
| 107 |
+
text-decoration: none;
|
| 108 |
+
font-family: 'Rajdhani', sans-serif;
|
| 109 |
+
font-weight: 700;
|
| 110 |
+
font-size: 0.85rem;
|
| 111 |
+
letter-spacing: 0.18em;
|
| 112 |
+
text-transform: uppercase;
|
| 113 |
+
border-radius: 2px;
|
| 114 |
+
transition: all 0.2s;
|
| 115 |
+
}
|
| 116 |
+
.product-card .plink:hover {
|
| 117 |
+
background: var(--trek-red);
|
| 118 |
+
color: #fff;
|
| 119 |
+
}
|
| 120 |
+
.product-section-title {
|
| 121 |
+
font-family: 'Rajdhani', sans-serif;
|
| 122 |
+
font-size: 0.8rem;
|
| 123 |
+
letter-spacing: 0.32em;
|
| 124 |
+
text-transform: uppercase;
|
| 125 |
+
color: rgba(255,255,255,0.45);
|
| 126 |
+
margin-bottom: 18px;
|
| 127 |
+
text-align: center;
|
| 128 |
+
}
|
| 129 |
+
.product-section-title .accent { color: var(--trek-red); }
|
| 130 |
body::before {
|
| 131 |
content: '';
|
| 132 |
position: fixed;
|
|
|
|
| 299 |
</style>
|
| 300 |
</head>
|
| 301 |
<body>
|
| 302 |
+
<div class="app">
|
| 303 |
+
<div class="col left">
|
| 304 |
+
<div class="brand">
|
| 305 |
+
<svg class="mark" viewBox="0 0 288.1 80" xmlns="http://www.w3.org/2000/svg" aria-label="Trek">
|
| 306 |
+
<g>
|
| 307 |
+
<polygon points="238.5,22.9 214.3,22.9 201.9,57.2 226.1,57.2 "/>
|
| 308 |
+
<polygon points="234.1,38.8 248.4,57.2 276.3,57.2 260.3,39.1 288.1,22.9 259.7,22.9 "/>
|
| 309 |
+
<path d="M206.6,29.5l2.4-6.6h-61.9l-8.5,23.6c-0.5,1.5-0.6,3.2,0.5,4.5c0.4,0.4,3.1,3.6,3.7,4.2c1,1.1,2.2,2,4.2,2h49.6l2.4-6.5h-34.7c-2.1,0-3.1-1.6-2.6-3.2l1.6-4.3h38.4l2.4-6.6h-38.4l2.6-7.1C168.3,29.4,206.6,29.4,206.6,29.5z"/>
|
| 310 |
+
<path d="M135.5,22.9H73.9L61.5,57.2h23.6l10-27.7h17.6c1.9,0,2.6,1.4,2.2,2.7c-0.4,1.4-1,3.1-1.5,4.1c-0.6,1.4-2,2.5-4.1,2.5s-16.5,0-16.5,0L107.1,57h27.1L122,43.3c0,0,4.9,0,7.3,0c3.4,0,5.4-1.8,6.3-4.1c1-2.6,3.3-8.9,4-11C140.9,25,138.9,22.9,135.5,22.9"/>
|
| 311 |
+
<polygon points="69.6,22.9 2.4,22.9 0,29.5 23.2,29.5 13.2,57.2 36,57.2 46,29.5 67.3,29.5 "/>
|
| 312 |
+
</g>
|
| 313 |
+
</svg>
|
| 314 |
+
<span class="label" lang="en">AI Assistant</span>
|
| 315 |
+
</div>
|
| 316 |
+
<h1>Sesli <span class="accent">Asistan</span></h1>
|
| 317 |
+
<div class="subtitle">Bisiklet · Stok · Fiyat · Anlık Yanıt</div>
|
| 318 |
|
| 319 |
+
<div class="stage">
|
| 320 |
+
<img id="avatarImg" src="/static/assistant.png" alt="Trek Asistan" />
|
| 321 |
+
<div id="ring" class="ring"></div>
|
| 322 |
+
<span class="corner tl"></span>
|
| 323 |
+
<span class="corner tr"></span>
|
| 324 |
+
<span class="corner bl"></span>
|
| 325 |
+
<span class="corner br"></span>
|
| 326 |
+
</div>
|
| 327 |
|
| 328 |
+
<div class="panel">
|
| 329 |
+
<div id="status" class="status disconnected">● Bağlantı Yok</div>
|
| 330 |
+
<div class="controls">
|
| 331 |
+
<button id="btnConnect">Konuşmaya Başla</button>
|
| 332 |
+
<button id="btnDisconnect" class="danger" disabled>Bitir</button>
|
| 333 |
+
</div>
|
| 334 |
</div>
|
| 335 |
+
|
| 336 |
+
<div class="footer"><span lang="en">Powered by Trek · Realtime AI</span></div>
|
| 337 |
</div>
|
| 338 |
|
| 339 |
+
<div class="col right">
|
| 340 |
+
<div class="product-section-title"><span class="accent">●</span> Ürün Vitrini</div>
|
| 341 |
+
<div class="product-card" id="productCard">
|
| 342 |
+
<div class="empty" id="productEmpty">
|
| 343 |
+
Bahsedilen ürün burada gösterilir
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
</div>
|
| 347 |
+
</div>
|
| 348 |
|
| 349 |
<script>
|
| 350 |
const SAMPLE_RATE = 24000;
|
|
|
|
| 476 |
ws.onerror = () => setStatus('Bağlantı hatası', 'disconnected');
|
| 477 |
}
|
| 478 |
|
| 479 |
+
function showProduct(p) {
|
| 480 |
+
const card = $('productCard');
|
| 481 |
+
if (!p || !p.image) return;
|
| 482 |
+
const linkHtml = p.link ? `<a class="plink" href="${p.link}" target="_blank" rel="noopener">Ürünü Gör</a>` : '';
|
| 483 |
+
card.innerHTML = `
|
| 484 |
+
<img class="pimg" src="${p.image}" alt="${p.name || ''}" onerror="this.style.display='none'" />
|
| 485 |
+
<div class="pname">${p.name || ''}</div>
|
| 486 |
+
${linkHtml}
|
| 487 |
+
`;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
function handleEvent(evt) {
|
| 491 |
switch (evt.type) {
|
| 492 |
+
case 'product.show':
|
| 493 |
+
if (evt.product) showProduct(evt.product);
|
| 494 |
+
break;
|
| 495 |
case 'response.audio.delta':
|
| 496 |
case 'response.output_audio.delta':
|
| 497 |
if (evt.delta) playPCM16(base64ToInt16(evt.delta));
|