SamiKoen commited on
Commit
fb67fbd
·
1 Parent(s): 24bc478

Add split-screen product display: avatar left, product image right

Browse files
Files changed (2) hide show
  1. app.py +82 -0
  2. 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 16px 40px;
35
- letter-spacing: 0.01em;
36
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  body::before {
38
  content: '';
39
  position: fixed;
@@ -206,39 +299,52 @@
206
  </style>
207
  </head>
208
  <body>
209
- <div class="brand">
210
- <svg class="mark" viewBox="0 0 288.1 80" xmlns="http://www.w3.org/2000/svg" aria-label="Trek">
211
- <g>
212
- <polygon points="238.5,22.9 214.3,22.9 201.9,57.2 226.1,57.2 "/>
213
- <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 "/>
214
- <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"/>
215
- <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"/>
216
- <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 "/>
217
- </g>
218
- </svg>
219
- <span class="label" lang="en">AI Assistant</span>
220
- </div>
221
- <h1>Sesli <span class="accent">Asistan</span></h1>
222
- <div class="subtitle">Bisiklet · Stok · Fiyat · Anlık Yanıt</div>
 
 
223
 
224
- <div class="stage">
225
- <img id="avatarImg" src="/static/assistant.png" alt="Trek Asistan" />
226
- <div id="ring" class="ring"></div>
227
- <span class="corner tl"></span>
228
- <span class="corner tr"></span>
229
- <span class="corner bl"></span>
230
- <span class="corner br"></span>
231
- </div>
232
 
233
- <div class="panel">
234
- <div id="status" class="status disconnected">● Bağlantı Yok</div>
235
- <div class="controls">
236
- <button id="btnConnect">Konuşmaya Başla</button>
237
- <button id="btnDisconnect" class="danger" disabled>Bitir</button>
 
238
  </div>
 
 
239
  </div>
240
 
241
- <div class="footer"><span lang="en">Powered by Trek · Realtime AI</span></div>
 
 
 
 
 
 
 
 
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));