Sinketji commited on
Commit
0964456
·
verified ·
1 Parent(s): 4cd620f

Update app/static/index.html

Browse files
Files changed (1) hide show
  1. app/static/index.html +637 -682
app/static/index.html CHANGED
@@ -1,704 +1,659 @@
1
- <!doctype html>
2
  <html lang="en">
3
  <head>
4
- <meta charset="utf-8"/>
5
- <meta name="viewport" content="width=device-width,initial-scale=1"/>
6
- <title>Perplexity HF Playground</title>
7
-
8
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/tokyo-night-dark.min.css">
9
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
10
-
11
- <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.2/marked.min.js"></script>
12
-
13
- <style>
14
- :root{
15
- --bg0:#070A12;
16
- --bg1:#0B1030;
17
- --card: rgba(255,255,255,.06);
18
- --stroke: rgba(255,255,255,.10);
19
- --text:#EAF0FF;
20
- --muted: rgba(234,240,255,.72);
21
-
22
- --neon1:#7C3AED;
23
- --neon2:#22D3EE;
24
- --neon3:#A3FF12;
25
-
26
- --shadow: 0 20px 70px rgba(0,0,0,.55);
27
- --radius: 18px;
28
- }
29
-
30
- *{ box-sizing:border-box; }
31
- html,body{ height:100%; }
32
- body{
33
- margin:0;
34
- font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
35
- color: var(--text);
36
- background:
37
- radial-gradient(1200px 800px at 10% 10%, rgba(124,58,237,.25), transparent 60%),
38
- radial-gradient(1200px 800px at 90% 20%, rgba(34,211,238,.20), transparent 60%),
39
- radial-gradient(1200px 800px at 50% 90%, rgba(163,255,18,.14), transparent 55%),
40
- linear-gradient(180deg, var(--bg0), var(--bg1));
41
- overflow-x:hidden;
42
- }
43
-
44
- .wrap{ max-width: 1100px; margin: 0 auto; padding: 18px 14px 34px; }
45
-
46
- .topbar{
47
- display:flex; gap:12px; align-items:center; justify-content:space-between;
48
- margin-bottom: 14px;
49
- }
50
-
51
- .brand{ display:flex; align-items:center; gap:10px; user-select:none; }
52
- .logo{
53
- width:38px;height:38px;border-radius:12px;
54
- background: linear-gradient(135deg, rgba(124,58,237,.9), rgba(34,211,238,.9));
55
- box-shadow: 0 0 0 1px rgba(255,255,255,.12), 0 18px 40px rgba(124,58,237,.20);
56
- }
57
- .brand h1{ font-size: 15px; letter-spacing: .3px; margin:0; line-height: 1.2; }
58
- .brand p{ margin:0; color: var(--muted); font-size: 12px; }
59
-
60
- .grid{ display:grid; grid-template-columns: 360px 1fr; gap: 14px; }
61
- @media (max-width: 980px){ .grid{ grid-template-columns: 1fr; } }
62
-
63
- .card{
64
- background: var(--card);
65
- border: 1px solid var(--stroke);
66
- border-radius: var(--radius);
67
- box-shadow: var(--shadow);
68
- backdrop-filter: blur(10px);
69
- overflow:hidden;
70
- }
71
- .card .hd{
72
- padding: 12px 12px;
73
- border-bottom: 1px solid rgba(255,255,255,.08);
74
- display:flex; align-items:center; justify-content:space-between; gap:10px;
75
- }
76
- .hd .t{ font-size: 13px; color: rgba(234,240,255,.92); letter-spacing: .25px; }
77
- .chip{
78
- font-size: 11px; padding: 5px 10px; border-radius: 999px;
79
- background: rgba(34,211,238,.10);
80
- border: 1px solid rgba(34,211,238,.20);
81
- color: rgba(234,240,255,.95);
82
- }
83
-
84
- .body{ padding: 12px; }
85
-
86
- label{ font-size: 12px; color: var(--muted); display:block; margin-bottom: 6px; }
87
- input, select, textarea{
88
- width:100%;
89
- color: var(--text);
90
- background: rgba(0,0,0,.28);
91
- border: 1px solid rgba(255,255,255,.12);
92
- border-radius: 12px;
93
- padding: 10px 10px;
94
- outline:none;
95
- }
96
- textarea{ min-height: 88px; resize: vertical; }
97
- input:focus, select:focus, textarea:focus{
98
- border-color: rgba(34,211,238,.45);
99
- box-shadow: 0 0 0 4px rgba(34,211,238,.10);
100
- }
101
- .row{ display:grid; grid-template-columns: 1fr 1fr; gap: 10px; }
102
- @media (max-width: 980px){ .row{ grid-template-columns: 1fr; } }
103
-
104
- .btn{
105
- width:100%;
106
- border: 1px solid rgba(255,255,255,.14);
107
- background: linear-gradient(135deg, rgba(124,58,237,.85), rgba(34,211,238,.85));
108
- padding: 11px 12px;
109
- border-radius: 14px;
110
- color: #061018;
111
- font-weight: 800;
112
- letter-spacing: .2px;
113
- cursor:pointer;
114
- box-shadow: 0 16px 40px rgba(34,211,238,.12);
115
- }
116
- .btn:disabled{ opacity:.55; cursor:not-allowed; }
117
-
118
- .subbtn{
119
- border: 1px solid rgba(255,255,255,.14);
120
- background: rgba(255,255,255,.06);
121
- padding: 10px 12px;
122
- border-radius: 14px;
123
- color: var(--text);
124
- cursor:pointer;
125
- }
126
-
127
- .chat{
128
- height: calc(100vh - 160px);
129
- min-height: 520px;
130
- display:flex;
131
- flex-direction:column;
132
- }
133
- .msgs{
134
- flex:1;
135
- overflow:auto;
136
- padding: 14px 12px;
137
- display:flex;
138
- flex-direction:column;
139
- gap: 10px;
140
- }
141
- .bubble{
142
- max-width: 92%;
143
- padding: 12px 12px;
144
- border-radius: 16px;
145
- border: 1px solid rgba(255,255,255,.12);
146
- background: rgba(0,0,0,.25);
147
- white-space: normal;
148
- }
149
- .me{
150
- align-self:flex-end;
151
- background: rgba(124,58,237,.18);
152
- border-color: rgba(124,58,237,.22);
153
- }
154
- .ai{
155
- align-self:flex-start;
156
- background: rgba(34,211,238,.10);
157
- border-color: rgba(34,211,238,.18);
158
- }
159
-
160
- .composer{
161
- padding: 12px;
162
- border-top: 1px solid rgba(255,255,255,.08);
163
- display:grid;
164
- grid-template-columns: 1fr 120px;
165
- gap: 10px;
166
- }
167
- @media (max-width: 520px){
168
- .composer{ grid-template-columns: 1fr; }
169
- }
170
-
171
- .tabs{
172
- display:flex;
173
- gap:8px;
174
- padding: 10px 12px 0;
175
- flex-wrap: wrap;
176
- }
177
- .tab{
178
- font-size: 12px;
179
- padding: 8px 10px;
180
- border-radius: 999px;
181
- border: 1px solid rgba(255,255,255,.12);
182
- background: rgba(255,255,255,.05);
183
- color: rgba(234,240,255,.88);
184
- cursor:pointer;
185
- }
186
- .tab.active{
187
- background: rgba(163,255,18,.12);
188
- border-color: rgba(163,255,18,.22);
189
- }
190
-
191
- /* Code canvas */
192
- pre{
193
- overflow:auto;
194
- padding: 0;
195
- margin: 10px 0;
196
- border-radius: 14px;
197
- border: 1px solid rgba(255,255,255,.10);
198
- background: rgba(0,0,0,.42);
199
- }
200
- .codeWrap{
201
- border-radius: 14px;
202
- border: 1px solid rgba(255,255,255,.10);
203
- background: rgba(0,0,0,.44);
204
- overflow:hidden;
205
- margin: 10px 0;
206
- }
207
- .codeTop{
208
- display:flex;
209
- align-items:center;
210
- justify-content:space-between;
211
- gap:10px;
212
- padding: 8px 10px;
213
- border-bottom: 1px solid rgba(255,255,255,.08);
214
- background: linear-gradient(90deg, rgba(124,58,237,.15), rgba(34,211,238,.10));
215
- }
216
- .dots{ display:flex; gap:6px; align-items:center; }
217
- .dot{ width:10px; height:10px; border-radius: 999px; opacity:.9; }
218
- .dot.r{ background:#ff5f56; }
219
- .dot.y{ background:#ffbd2e; }
220
- .dot.g{ background:#27c93f; }
221
- .copyBtn{
222
- border: 1px solid rgba(255,255,255,.12);
223
- background: rgba(255,255,255,.06);
224
- color: rgba(234,240,255,.92);
225
- padding: 6px 10px;
226
- border-radius: 10px;
227
- font-size: 12px;
228
- cursor:pointer;
229
- }
230
- .small{ font-size: 11px; color: rgba(234,240,255,.68); }
231
- .mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
232
- </style>
233
  </head>
234
-
235
  <body>
236
- <div class="wrap">
237
- <div class="topbar">
238
- <div class="brand">
239
- <div class="logo"></div>
240
- <div>
241
- <h1>Perplexity HF Playground</h1>
242
- <p>HF Docker proxy + code canvas + citations</p>
243
- </div>
244
- </div>
245
- <span class="chip" id="statusChip">Idle</span>
246
- </div>
247
 
248
- <div class="grid">
249
- <div class="card">
250
- <div class="hd">
251
- <div class="t">Settings</div>
252
- <button class="subbtn" id="btnLoadModels">Reload models</button>
 
 
253
  </div>
254
- <div class="body">
255
- <label>Backend URL (optional)</label>
256
- <input id="backendUrl" placeholder="Leave empty if UI served from same Space" />
257
 
258
- <div style="height:10px"></div>
259
 
260
- <label>Perplexity API Key (frontend)</label>
261
- <input id="apiKey" type="password" placeholder="pplx-..." />
262
-
263
- <div style="height:10px"></div>
264
 
265
- <div class="row">
266
- <div>
267
- <label>Model</label>
268
- <select id="model"></select>
 
 
269
  </div>
270
- <div>
271
- <label>Temperature</label>
272
- <input id="temp" type="number" step="0.1" min="0" max="2" value="0.2"/>
273
- </div>
274
- </div>
275
 
276
- <div style="height:10px"></div>
 
 
 
 
 
277
 
278
- <label>System prompt</label>
279
- <textarea id="systemPrompt">You are a precise, helpful assistant. Output clean answers and include code when needed.</textarea>
 
 
 
 
280
 
281
- <div style="height:10px"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
- <div class="row">
284
- <div>
285
- <label>Upload (PDF/TXT)</label>
286
- <input id="file" type="file" accept=".pdf,.txt,text/plain,application/pdf"/>
287
- <div class="small" style="margin-top:6px">PDF/TXT will be extracted on the server and appended to the prompt.</div>
288
- </div>
289
- <div>
290
- <label>Streaming</label>
291
- <select id="stream">
292
- <option value="false" selected>Off (stable)</option>
293
- <option value="true">On (SSE passthrough)</option>
294
- </select>
295
- <div class="small" style="margin-top:6px">Streaming is best-effort; if glitchy, keep it OFF.</div>
296
- </div>
297
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
- <div style="height:12px"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
- <button class="btn" id="btnClear">Clear chat</button>
302
- </div>
303
- </div>
 
 
 
304
 
305
- <div class="card chat">
306
- <div class="hd">
307
- <div class="t">Chat</div>
308
- <div class="small" id="hint">Tip: ask for code; it will render in a colorful canvas.</div>
309
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
 
311
- <div class="tabs">
312
- <button class="tab active" data-tab="answer">Answer</button>
313
- <button class="tab" data-tab="citations">Citations</button>
314
- <button class="tab" data-tab="raw">Raw JSON</button>
315
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
- <div class="msgs" id="msgs"></div>
 
 
 
 
318
 
319
- <div class="composer">
320
- <textarea id="prompt" placeholder="Ask anything… (Shift+Enter for newline)"></textarea>
321
- <button class="btn" id="btnSend">Send</button>
322
- </div>
323
- </div>
324
- </div>
325
- </div>
326
-
327
- <script>
328
- const els = {
329
- backendUrl: document.getElementById('backendUrl'),
330
- apiKey: document.getElementById('apiKey'),
331
- model: document.getElementById('model'),
332
- temp: document.getElementById('temp'),
333
- systemPrompt: document.getElementById('systemPrompt'),
334
- file: document.getElementById('file'),
335
- stream: document.getElementById('stream'),
336
- btnSend: document.getElementById('btnSend'),
337
- btnClear: document.getElementById('btnClear'),
338
- btnLoadModels: document.getElementById('btnLoadModels'),
339
- msgs: document.getElementById('msgs'),
340
- prompt: document.getElementById('prompt'),
341
- statusChip: document.getElementById('statusChip'),
342
- tabs: [...document.querySelectorAll('.tab')],
343
- };
344
-
345
- const state = {
346
- tab: "answer",
347
- lastRaw: null,
348
- lastCitations: [],
349
- // conversation history WITHOUT system; we will always prepend system at request time
350
- messages: [], // {role, content} alternating user/assistant
351
- };
352
-
353
- function setStatus(text){ els.statusChip.textContent = text; }
354
-
355
- function baseUrl(){
356
- let u = els.backendUrl.value.trim();
357
- if (!u) u = location.origin; // same Space by default
358
- return u.replace(/\/+$/,'');
359
- }
360
-
361
- function esc(s){
362
- return (s ?? "").replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
363
- }
364
-
365
- function addBubble(role, html){
366
- const div = document.createElement('div');
367
- div.className = 'bubble ' + (role === 'user' ? 'me' : 'ai');
368
- div.innerHTML = html;
369
- els.msgs.appendChild(div);
370
- els.msgs.scrollTop = els.msgs.scrollHeight;
371
- return div;
372
- }
373
-
374
- function renderMarkdownInto(container, text){
375
- const raw = text || "";
376
- let think = "";
377
- let answer = raw;
378
-
379
- const start = raw.indexOf("<think>");
380
- const end = raw.indexOf("</think>");
381
- if (start !== -1 && end !== -1 && end > start){
382
- think = raw.slice(start + 7, end).trim();
383
- answer = (raw.slice(0, start) + raw.slice(end + 8)).trim();
384
- }
385
-
386
- container.innerHTML = marked.parse(answer);
387
-
388
- if (think){
389
- const details = document.createElement('details');
390
- details.style.marginTop = "10px";
391
- details.innerHTML = `
392
- <summary class="small">Thinking (model internal)</summary>
393
- <pre class="mono" style="padding:12px;margin:10px 0;background:rgba(0,0,0,.35);border-radius:14px;border:1px solid rgba(255,255,255,.10)">${esc(think)}</pre>
394
- `;
395
- container.appendChild(details);
396
- }
397
-
398
- decorateCodeBlocks(container);
399
- }
400
-
401
- function decorateCodeBlocks(root){
402
- const pres = root.querySelectorAll('pre code');
403
- pres.forEach(code => {
404
- hljs.highlightElement(code);
405
- const pre = code.parentElement;
406
-
407
- // already wrapped?
408
- if (pre.parentElement && pre.parentElement.classList && pre.parentElement.classList.contains('codeWrap')) return;
409
-
410
- const wrap = document.createElement('div');
411
- wrap.className = 'codeWrap';
412
-
413
- const top = document.createElement('div');
414
- top.className = 'codeTop';
415
- top.innerHTML = `
416
- <div class="dots">
417
- <span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
418
- <span class="small" style="margin-left:8px">code</span>
419
- </div>
420
- `;
421
-
422
- const btn = document.createElement('button');
423
- btn.className = 'copyBtn';
424
- btn.textContent = 'Copy';
425
- btn.onclick = async () => {
426
- await navigator.clipboard.writeText(code.innerText);
427
- const old = btn.textContent;
428
- btn.textContent = 'Copied';
429
- setTimeout(() => btn.textContent = old, 1200);
430
- };
431
-
432
- top.appendChild(btn);
433
-
434
- pre.parentNode.insertBefore(wrap, pre);
435
- wrap.appendChild(top);
436
- wrap.appendChild(pre);
437
- });
438
- }
439
-
440
- function syncTabs(){
441
- els.tabs.forEach(t => t.classList.toggle('active', t.dataset.tab === state.tab));
442
- renderPanels();
443
- }
444
-
445
- function renderPanels(){
446
- [...els.msgs.querySelectorAll('[data-panel="1"]')].forEach(n => n.remove());
447
-
448
- if (state.tab === "citations"){
449
- const html = (state.lastCitations?.length)
450
- ? `<div><div class="small">Citations</div><ol>${state.lastCitations.map(u => `<li><a href="${esc(u)}" target="_blank" rel="noreferrer" style="color:#A3FF12">${esc(u)}</a></li>`).join('')}</ol></div>`
451
- : `<div class="small">No citations found in last response.</div>`;
452
- const b = addBubble('assistant', html);
453
- b.dataset.panel = "1";
454
- }
455
-
456
- if (state.tab === "raw"){
457
- const raw = state.lastRaw ? JSON.stringify(state.lastRaw, null, 2) : "No raw response yet.";
458
- const b = addBubble('assistant', `<div class="small">Raw JSON</div><pre class="mono" style="padding:12px">${esc(raw)}</pre>`);
459
- b.dataset.panel = "1";
460
- }
461
- }
462
-
463
- async function loadModels(){
464
- const url = baseUrl() + "/api/models";
465
- const res = await fetch(url);
466
- const data = await res.json();
467
- if (!data.ok) throw new Error("Failed to load models");
468
- els.model.innerHTML = data.models.map(m => `<option value="${esc(m)}">${esc(m)}</option>`).join('');
469
- }
470
-
471
- // ---- Build messages for request (Correct logic maintained) ----
472
- function buildMessagesForRequest(userText){
473
- const system = els.systemPrompt.value;
474
- // Prepend system prompt to the conversation history
475
- return [{role:"system", content: system}, ...state.messages, {role:"user", content: userText}];
476
- }
477
-
478
- async function sendNonStreaming(userText){
479
- const url = baseUrl();
480
- const key = els.apiKey.value.trim();
481
- const model = els.model.value;
482
- const temperature = parseFloat(els.temp.value || "0.2");
483
-
484
- const file = els.file.files && els.file.files[0] ? els.file.files[0] : null;
485
-
486
- if (file){
487
- // For file upload, use the specific /api/chat-upload endpoint (note: may not support multi-turn history depending on backend implementation)
488
- const form = new FormData();
489
- form.append("api_key", key);
490
- form.append("model", model);
491
- form.append("prompt", userText);
492
- form.append("system_prompt", els.systemPrompt.value);
493
- form.append("stream", "false");
494
- form.append("file", file);
495
-
496
- const res = await fetch(url + "/api/chat-upload", { method:"POST", body: form });
497
- const data = await res.json();
498
- if (!data.ok) throw new Error(data.detail || "Request failed");
499
- return data;
500
- } else {
501
- // Standard chat endpoint
502
- const payload = {
503
- api_key: key,
504
- model,
505
- messages: buildMessagesForRequest(userText),
506
- temperature,
507
- stream: false
508
- };
509
- const res = await fetch(url + "/api/chat", {
510
- method:"POST",
511
- headers:{ "Content-Type":"application/json" },
512
- body: JSON.stringify(payload)
513
- });
514
- const data = await res.json();
515
- if (!data.ok) throw new Error(data.detail || "Request failed");
516
- return data;
517
- }
518
- }
519
-
520
- async function sendStreaming(userText){
521
- const url = baseUrl();
522
- const key = els.apiKey.value.trim();
523
- const model = els.model.value;
524
- const temperature = parseFloat(els.temp.value || "0.2");
525
-
526
- const payload = {
527
- api_key: key,
528
- model,
529
- messages: buildMessagesForRequest(userText),
530
- temperature,
531
- stream: true
532
- };
533
-
534
- const res = await fetch(url + "/api/chat", {
535
- method:"POST",
536
- headers:{ "Content-Type":"application/json" },
537
- body: JSON.stringify(payload)
538
- });
539
-
540
- if (!res.ok) throw new Error("Streaming request failed");
541
-
542
- // Streaming bubble (single bubble only; onSend will NOT add another assistant bubble)
543
- const bubble = addBubble('assistant',
544
- `<div class="small" id="streamLabel">Streaming…</div><div class="streamBox"></div>`
545
- );
546
- const streamLabel = bubble.querySelector("#streamLabel");
547
- const streamBox = bubble.querySelector(".streamBox");
548
-
549
- const reader = res.body.getReader();
550
- const decoder = new TextDecoder();
551
-
552
- let buf = "";
553
- let combined = "";
554
-
555
- while(true){
556
- const {value, done} = await reader.read();
557
- if (done) break;
558
- buf += decoder.decode(value, {stream:true});
559
-
560
- const lines = buf.split("\n");
561
- buf = lines.pop() || "";
562
-
563
- for (const line of lines){
564
- const trimmed = line.trim();
565
- if (!trimmed) continue;
566
-
567
- let jsonText = trimmed.startsWith("data:") ? trimmed.slice(5).trim() : trimmed;
568
- if (jsonText === "[DONE]") continue;
569
-
570
- try{
571
- const obj = JSON.parse(jsonText);
572
- const delta = obj?.choices?.[0]?.delta?.content;
573
- const msg = obj?.choices?.[0]?.message?.content;
574
-
575
- if (delta) combined += delta;
576
- else if (msg) combined = msg; // Final message in some non-chunked scenarios
577
-
578
- // Update UI
579
- streamBox.innerHTML = marked.parse(combined);
580
- decorateCodeBlocks(streamBox);
581
- els.msgs.scrollTop = els.msgs.scrollHeight;
582
- }catch(e){
583
- // ignore non-JSON lines
584
  }
585
- }
586
- }
587
-
588
- streamLabel.textContent = "Answer";
589
- // Return the final combined content for history update
590
- return { ok:true, content: combined, citations: [], raw: { streaming: true } };
591
- }
592
-
593
- // --- UPDATED onSend FUNCTION with fixes ---
594
- async function onSend(){
595
- const userText = els.prompt.value.trim();
596
- if (!userText) return;
597
-
598
- const key = els.apiKey.value.trim();
599
- if (!key){
600
- alert("Enter API key first.");
601
- return;
602
- }
603
-
604
- els.btnSend.disabled = true;
605
- setStatus("Working");
606
-
607
- // 1. Show user bubble immediately (but DON'T add to history yet)
608
- addBubble('user', `<div>${esc(userText)}</div>`);
609
- els.prompt.value = ""; // Clear prompt immediately
610
-
611
- // Check if a file was selected before the call
612
- const fileSelected = els.file.files.length > 0;
613
-
614
- try{
615
- const useStream = (els.stream.value === "true");
616
-
617
- // 2. Perform API request
618
- const data = useStream ? await sendStreaming(userText) : await sendNonStreaming(userText);
619
-
620
- // 3. Save panels data
621
- state.lastRaw = data.raw || data;
622
- state.lastCitations = data.citations || [];
623
-
624
- // 4. Render assistant only in non-stream mode (stream already rendered live)
625
- if (!useStream){
626
- const container = document.createElement('div');
627
- renderMarkdownInto(container, data.content || "");
628
- addBubble('assistant', container.innerHTML);
629
- }
630
-
631
- // 5. Update history ONLY after successful response (FIX)
632
- state.messages.push({ role:"user", content: userText });
633
- state.messages.push({ role:"assistant", content: data.content || "" });
634
-
635
- // 6. Clear File Input after successful use (Improvement)
636
- if (fileSelected) {
637
- els.file.value = '';
638
- }
639
-
640
- syncTabs();
641
-
642
- }catch(err){
643
- // 7. Handle Error and show error bubble
644
- addBubble('assistant', `<div class="small">Error</div><pre class="mono" style="padding:12px">${esc(err?.message || String(err))}</pre>`);
645
-
646
- // 8. Error hone par user prompt ko wapas input field mein daalna (UX Improvement)
647
- els.prompt.value = userText;
648
-
649
- }finally{
650
- els.btnSend.disabled = false;
651
- setStatus("Idle");
652
- }
653
- }
654
-
655
- els.btnSend.addEventListener('click', onSend);
656
- els.prompt.addEventListener('keydown', (e)=>{
657
- if (e.key === "Enter" && !e.shiftKey){
658
- e.preventDefault();
659
- onSend();
660
- }
661
- });
662
-
663
- els.btnClear.addEventListener('click', ()=>{
664
- state.messages = [];
665
- state.lastRaw = null;
666
- state.lastCitations = [];
667
- els.msgs.innerHTML = "";
668
- els.file.value = ''; // Clear file input as well
669
- setStatus("Idle");
670
- });
671
-
672
- els.tabs.forEach(btn => btn.addEventListener('click', ()=>{
673
- state.tab = btn.dataset.tab;
674
- syncTabs();
675
- }));
676
-
677
- els.btnLoadModels.addEventListener('click', async ()=>{
678
- try{
679
- setStatus("Loading models");
680
- await loadModels();
681
- setStatus("Idle");
682
- }catch(e){
683
- setStatus("Model load failed");
684
- alert("Failed to load models. Check backend URL (or keep it empty if same Space).");
685
- }
686
- });
687
-
688
- (async function init(){
689
- // Load saved settings from localStorage
690
- els.backendUrl.value = localStorage.getItem("pplx_backend") || "";
691
- els.apiKey.value = localStorage.getItem("pplx_key") || "";
692
- els.systemPrompt.value = localStorage.getItem("pplx_system") || els.systemPrompt.value;
693
-
694
- // Save settings when they change
695
- els.backendUrl.addEventListener('change', ()=> localStorage.setItem("pplx_backend", els.backendUrl.value.trim()));
696
- els.apiKey.addEventListener('change', ()=> localStorage.setItem("pplx_key", els.apiKey.value.trim()));
697
- els.systemPrompt.addEventListener('change', ()=> localStorage.setItem("pplx_system", els.systemPrompt.value));
698
-
699
- // Initial model load
700
- try{ await loadModels(); } catch(_){ /* ok */ }
701
- })();
702
- </script>
703
  </body>
704
- </html>
 
1
+ <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Blox City RP - Ultimate Edition</title>
7
+ <style>
8
+ body { margin: 0; overflow: hidden; background: #87CEEB; font-family: 'Verdana', sans-serif; user-select: none; -webkit-user-select: none; }
9
+ #game-ui { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
10
+
11
+ /* HUD */
12
+ #hud-top { position: absolute; top: 10px; left: 10px; right: 10px; display: flex; justify-content: space-between; pointer-events: auto; }
13
+ .stat-box { background: rgba(0, 0, 0, 0.7); color: white; padding: 10px 15px; border-radius: 12px; font-weight: bold; border: 2px solid #fff; box-shadow: 0 4px 6px rgba(0,0,0,0.5); display: flex; align-items: center; gap: 8px; font-size: 14px; }
14
+
15
+ /* ALERTS */
16
+ #message-area { position: absolute; top: 15%; width: 100%; text-align: center; color: #fff; font-size: 24px; text-shadow: 2px 2px 0 #000; font-weight: 900; pointer-events: none; opacity: 0; transition: opacity 0.5s; z-index: 20; }
17
+
18
+ /* CONTROLS */
19
+ .controls-area { position: absolute; bottom: 20px; pointer-events: auto; display: flex; gap: 15px; }
20
+ #controls-left { left: 20px; flex-direction: column; align-items: center; }
21
+ #controls-right { right: 20px; align-items: flex-end; flex-direction: column-reverse; }
22
+
23
+ .btn { background: rgba(255, 255, 255, 0.25); backdrop-filter: blur(5px); border: 2px solid rgba(255,255,255,0.6); border-radius: 50%; color: white; display: flex; justify-content: center; align-items: center; font-size: 20px; touch-action: manipulation; box-shadow: 0 4px 10px rgba(0,0,0,0.4); transition: transform 0.1s; cursor: pointer; }
24
+ .btn:active { transform: scale(0.9); background: rgba(255, 255, 255, 0.5); }
25
+
26
+ .dpad-row { display: flex; gap: 8px; }
27
+ .dpad-btn { width: 55px; height: 55px; }
28
+
29
+ .action-btn { width: 70px; height: 70px; margin-bottom: 15px; background: rgba(255, 193, 7, 0.8); border-color: #FFD54F; font-weight: bold; font-size: 12px; flex-direction: column; text-align: center; line-height: 1.1; color: #000; }
30
+ .jump-btn { width: 75px; height: 75px; background: rgba(33, 150, 243, 0.6); border-color: #64B5F6; }
31
+ .summon-btn { width: 50px; height: 50px; background: rgba(156, 39, 176, 0.6); border-color: #BA68C8; font-size: 20px; margin-bottom: 10px; }
32
+
33
+ /* COOKING UI */
34
+ #cooking-container { display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 200px; background: #222; padding: 15px; border-radius: 10px; border: 3px solid #fff; pointer-events: none; text-align: center; color: white; z-index: 50; }
35
+ #cooking-bar-bg { width: 100%; height: 15px; background: #444; margin-top: 5px; border-radius: 10px; overflow: hidden; }
36
+ #cooking-bar-fill { width: 0%; height: 100%; background: #00E676; }
37
+
38
+ /* VEHICLE MENU */
39
+ #vehicle-menu { display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 350px; max-height: 60vh; background: #fff; border-radius: 15px; box-shadow: 0 20px 50px rgba(0,0,0,0.8); overflow-y: auto; pointer-events: auto; padding: 15px; z-index: 100; font-family: sans-serif; }
40
+ #vehicle-menu h3 { margin: 0 0 10px 0; text-align: center; color: #333; border-bottom: 2px solid #eee; padding-bottom: 5px; }
41
+ .v-item { display: flex; align-items: center; justify-content: space-between; padding: 10px; border-bottom: 1px solid #f0f0f0; }
42
+ .v-btn { background: #2196F3; color: white; border: none; padding: 6px 12px; border-radius: 15px; font-weight: bold; cursor: pointer; font-size: 12px; }
43
+ #close-menu { display: block; width: 100%; padding: 10px; margin-top: 15px; background: #f44336; color: white; border: none; border-radius: 8px; font-weight: bold; }
44
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  </head>
 
46
  <body>
 
 
 
 
 
 
 
 
 
 
 
47
 
48
+ <!-- UI LAYER -->
49
+ <div id="game-ui">
50
+ <div id="hud-top">
51
+ <div class="stat-box">💵 $<span id="money-display">500</span></div>
52
+ <div class="stat-box" style="flex:1; justify-content: center; margin: 0 10px;">
53
+ <span id="job-display">🏠 Explore City</span>
54
+ </div>
55
  </div>
 
 
 
56
 
57
+ <div id="message-area">Welcome to Blox City!</div>
58
 
59
+ <div id="cooking-container">
60
+ COOKING...
61
+ <div id="cooking-bar-bg"><div id="cooking-bar-fill"></div></div>
62
+ </div>
63
 
64
+ <div class="controls-area" id="controls-left">
65
+ <div class="dpad-row"><div class="btn dpad-btn" id="btn-up">⬆️</div></div>
66
+ <div class="dpad-row">
67
+ <div class="btn dpad-btn" id="btn-left">⬅️</div>
68
+ <div class="btn dpad-btn" id="btn-down">⬇️</div>
69
+ <div class="btn dpad-btn" id="btn-right">➡️</div>
70
  </div>
71
+ </div>
 
 
 
 
72
 
73
+ <div class="controls-area" id="controls-right">
74
+ <div class="btn summon-btn" id="btn-summon">🚗</div>
75
+ <div class="btn action-btn" id="context-btn">Action</div>
76
+ <div class="btn jump-btn" id="btn-jump">JUMP</div>
77
+ </div>
78
+ </div>
79
 
80
+ <!-- MENUS -->
81
+ <div id="vehicle-menu">
82
+ <h3>Garage (All Free!)</h3>
83
+ <div id="vehicle-list"></div>
84
+ <button id="close-menu">Close</button>
85
+ </div>
86
 
87
+ <!-- 3D ENGINE -->
88
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
89
+ <script>
90
+ /** --- CONFIG --- */
91
+ const STATE = {
92
+ money: 500,
93
+ isCooking: false,
94
+ holdingFood: false,
95
+ currentOrder: null,
96
+ inVehicle: null,
97
+ canJump: true,
98
+ velocity: new THREE.Vector3(),
99
+ keys: { w: false, a: false, s: false, d: false },
100
+ ownedVehicles: ['Scooter', 'Sedan', 'SportBike', 'SuperCar', 'Truck'], // All unlocked
101
+ };
102
+
103
+ const VEHICLES_DB = [
104
+ { name: 'Scooter', speed: 0.8, color: 0xFF5722, type: 'bike' },
105
+ { name: 'SportBike', speed: 1.5, color: 0x00E676, type: 'bike' },
106
+ { name: 'Sedan', speed: 1.2, color: 0x2196F3, type: 'car' },
107
+ { name: 'Truck', speed: 1.0, color: 0x5D4037, type: 'car' },
108
+ { name: 'SuperCar', speed: 2.2, color: 0xD50000, type: 'car' },
109
+ { name: 'Police', speed: 1.8, color: 0x111111, type: 'car' }
110
+ ];
111
+
112
+ /** --- SCENE SETUP --- */
113
+ const scene = new THREE.Scene();
114
+ scene.background = new THREE.Color(0x87CEEB);
115
+ scene.fog = new THREE.Fog(0x87CEEB, 20, 120);
116
+
117
+ const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
118
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
119
+ renderer.setSize(window.innerWidth, window.innerHeight);
120
+ renderer.shadowMap.enabled = true;
121
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
122
+ document.body.appendChild(renderer.domElement);
123
+
124
+ // Lights
125
+ const ambLight = new THREE.AmbientLight(0xffffff, 0.6);
126
+ scene.add(ambLight);
127
+ const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
128
+ dirLight.position.set(50, 100, 50);
129
+ dirLight.castShadow = true;
130
+ dirLight.shadow.mapSize.width = 2048;
131
+ dirLight.shadow.mapSize.height = 2048;
132
+ dirLight.shadow.camera.left = -60; dirLight.shadow.camera.right = 60;
133
+ dirLight.shadow.camera.top = 60; dirLight.shadow.camera.bottom = -60;
134
+ scene.add(dirLight);
135
+
136
+ /** --- TEXTURE GENERATORS --- */
137
+ function createTexture(color, type='plain') {
138
+ const cvs = document.createElement('canvas');
139
+ cvs.width = 64; cvs.height = 64;
140
+ const ctx = cvs.getContext('2d');
141
+ ctx.fillStyle = color;
142
+ ctx.fillRect(0,0,64,64);
143
+
144
+ if(type === 'brick') {
145
+ ctx.fillStyle = 'rgba(0,0,0,0.1)';
146
+ ctx.fillRect(0, 0, 64, 2); ctx.fillRect(0, 32, 64, 2);
147
+ ctx.fillRect(32, 0, 2, 32); ctx.fillRect(0, 32, 2, 32);
148
+ }
149
+ if(type === 'checkered') {
150
+ ctx.fillStyle = '#ffffff';
151
+ ctx.fillRect(0,0,32,32); ctx.fillRect(32,32,32,32);
152
+ ctx.fillStyle = '#000000';
153
+ ctx.fillRect(32,0,32,32); ctx.fillRect(0,32,32,32);
154
+ }
155
+ if(type === 'face') {
156
+ ctx.fillStyle = '#FFCC80'; ctx.fillRect(0,0,64,64);
157
+ ctx.fillStyle = 'black';
158
+ ctx.fillRect(15, 20, 8, 8); // Eye L
159
+ ctx.fillRect(41, 20, 8, 8); // Eye R
160
+ ctx.fillRect(20, 45, 24, 4); // Mouth
161
+ }
162
+
163
+ const tex = new THREE.CanvasTexture(cvs);
164
+ tex.magFilter = THREE.NearestFilter;
165
+ return tex;
166
+ }
167
 
168
+ const MATS = {
169
+ grass: new THREE.MeshLambertMaterial({ color: 0x4CAF50 }),
170
+ road: new THREE.MeshLambertMaterial({ color: 0x333333 }),
171
+ sidewalk: new THREE.MeshLambertMaterial({ color: 0x9E9E9E }),
172
+ skin: new THREE.MeshLambertMaterial({ color: 0xFFCC80 }),
173
+ face: new THREE.MeshLambertMaterial({ map: createTexture(null, 'face') }),
174
+ brickRed: new THREE.MeshLambertMaterial({ map: createTexture('#E57373', 'brick') }),
175
+ brickBlue: new THREE.MeshLambertMaterial({ map: createTexture('#64B5F6', 'brick') }),
176
+ floorCheck: new THREE.MeshLambertMaterial({ map: createTexture(null, 'checkered') }),
177
+ glass: new THREE.MeshPhongMaterial({ color: 0x81D4FA, transparent: true, opacity: 0.6, shininess: 100 }),
178
+ wood: new THREE.MeshLambertMaterial({ color: 0x8D6E63 }),
179
+ metal: new THREE.MeshStandardMaterial({ color: 0xAAAAAA, roughness: 0.2 })
180
+ };
181
+ MATS.floorCheck.map.repeat.set(4,4);
182
+ MATS.floorCheck.map.wrapS = THREE.RepeatWrapping;
183
+ MATS.floorCheck.map.wrapT = THREE.RepeatWrapping;
184
+
185
+ /** --- WORLD BUILDER --- */
186
+ // Ground
187
+ const ground = new THREE.Mesh(new THREE.PlaneGeometry(400, 400), MATS.grass);
188
+ ground.rotation.x = -Math.PI/2;
189
+ ground.receiveShadow = true;
190
+ scene.add(ground);
191
+
192
+ // Road Helper
193
+ function addRoad(x, z, w, l, rotated=false) {
194
+ const r = new THREE.Mesh(new THREE.PlaneGeometry(w, l), MATS.road);
195
+ r.rotation.x = -Math.PI/2;
196
+ if(rotated) r.rotation.z = Math.PI/2;
197
+ r.position.set(x, 0.02, z);
198
+ r.receiveShadow = true;
199
+ scene.add(r);
200
+
201
+ // Sidewalks
202
+ const sw1 = new THREE.Mesh(new THREE.PlaneGeometry(2, l), MATS.sidewalk);
203
+ sw1.rotation.x = -Math.PI/2;
204
+ if(rotated) sw1.rotation.z = Math.PI/2;
205
+ // Calculations simplified for demo
206
+ if(rotated) sw1.position.set(x, 0.03, z - w/2 - 1);
207
+ else sw1.position.set(x - w/2 - 1, 0.03, z);
208
+ scene.add(sw1);
209
+
210
+ const sw2 = sw1.clone();
211
+ if(rotated) sw2.position.set(x, 0.03, z + w/2 + 1);
212
+ else sw2.position.set(x + w/2 + 1, 0.03, z);
213
+ scene.add(sw2);
214
+ }
215
 
216
+ // Layout: Main Road (Z axis), Cross Road (X axis)
217
+ addRoad(0, 0, 14, 400);
218
+ addRoad(0, 0, 400, 14, true);
219
+
220
+ // --- PARK (Green Zone) ---
221
+ const parkGroup = new THREE.Group();
222
+ parkGroup.position.set(40, 0, 40);
223
+
224
+ const parkBase = new THREE.Mesh(new THREE.BoxGeometry(50, 0.5, 50), MATS.grass);
225
+ parkBase.position.y = 0.25;
226
+ parkGroup.add(parkBase);
227
+
228
+ // Trees
229
+ for(let i=0; i<8; i++) {
230
+ const tree = new THREE.Group();
231
+ const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.7, 3), MATS.wood);
232
+ trunk.position.y = 1.5;
233
+ const leaves = new THREE.Mesh(new THREE.ConeGeometry(3, 6, 8), new THREE.MeshLambertMaterial({color:0x2E7D32}));
234
+ leaves.position.y = 5;
235
+ tree.add(trunk, leaves);
236
+ tree.position.set((Math.random()-0.5)*40, 0, (Math.random()-0.5)*40);
237
+ parkGroup.add(tree);
238
+ }
239
+ // Bench
240
+ const bench = new THREE.Group();
241
+ const seat = new THREE.Mesh(new THREE.BoxGeometry(4, 0.2, 1), MATS.wood);
242
+ seat.position.y = 1;
243
+ const leg1 = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1, 1), MATS.metal);
244
+ leg1.position.set(-1.8, 0.5, 0);
245
+ const leg2 = leg1.clone(); leg2.position.set(1.8, 0.5, 0);
246
+ bench.add(seat, leg1, leg2);
247
+ bench.position.set(0, 0, 0);
248
+ parkGroup.add(bench);
249
+
250
+ scene.add(parkGroup);
251
+
252
+ // --- RESTAURANT (Detailed Interior) ---
253
+ const restGroup = new THREE.Group();
254
+ restGroup.position.set(-40, 0, 40);
255
+
256
+ // Floor
257
+ const floor = new THREE.Mesh(new THREE.BoxGeometry(30, 0.2, 20), MATS.floorCheck);
258
+ floor.position.y = 0.1;
259
+ restGroup.add(floor);
260
+
261
+ // Walls (leaving gaps)
262
+ const wallMat = new THREE.MeshLambertMaterial({color: 0xFFEB3B});
263
+ const wallBack = new THREE.Mesh(new THREE.BoxGeometry(30, 8, 1), wallMat);
264
+ wallBack.position.set(0, 4, -10);
265
+ const wallLeft = new THREE.Mesh(new THREE.BoxGeometry(1, 8, 20), wallMat);
266
+ wallLeft.position.set(-15, 4, 0);
267
+ const wallRight = new THREE.Mesh(new THREE.BoxGeometry(1, 8, 20), wallMat);
268
+ wallRight.position.set(15, 4, 0);
269
+ const roof = new THREE.Mesh(new THREE.BoxGeometry(32, 1, 22), new THREE.MeshLambertMaterial({color: 0xFBC02D}));
270
+ roof.position.y = 8.5;
271
+
272
+ restGroup.add(wallBack, wallLeft, wallRight, roof);
273
+
274
+ // Kitchen
275
+ const counter = new THREE.Mesh(new THREE.BoxGeometry(15, 2.5, 2), new THREE.MeshLambertMaterial({color:0xffffff}));
276
+ counter.position.set(0, 1.25, -4);
277
+ restGroup.add(counter);
278
+
279
+ // Machines
280
+ const grill = new THREE.Mesh(new THREE.BoxGeometry(3, 2, 2), new THREE.MeshLambertMaterial({color:0x333333}));
281
+ grill.position.set(-3, 1.5, -9);
282
+ restGroup.add(grill); // Grill at back wall
283
+
284
+ const fryer = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), new THREE.MeshLambertMaterial({color:0xCCCCCC}));
285
+ fryer.position.set(2, 1.5, -9);
286
+ restGroup.add(fryer);
287
+
288
+ // Tables & Chairs
289
+ function addTable(x, z) {
290
+ const t = new THREE.Mesh(new THREE.CylinderGeometry(2, 2, 1.5), new THREE.MeshLambertMaterial({color:0xD32F2F}));
291
+ t.position.set(x, 0.75, z);
292
+ restGroup.add(t);
293
+ }
294
+ addTable(-8, 5); addTable(8, 5); addTable(-8, 0); addTable(8, 0);
295
+
296
+ // Sign
297
+ const sign = new THREE.Mesh(new THREE.BoxGeometry(20, 3, 1), new THREE.MeshBasicMaterial({color:0xD50000}));
298
+ sign.position.set(0, 10, 10);
299
+ restGroup.add(sign);
300
+
301
+ // NPC
302
+ function createNPC(x, z, color) {
303
+ const npc = new THREE.Group();
304
+ const nBody = new THREE.Mesh(new THREE.BoxGeometry(0.8, 1, 0.4), new THREE.MeshLambertMaterial({color: color}));
305
+ nBody.position.y = 1.5;
306
+ const nHead = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), MATS.skin);
307
+ nHead.position.y = 2.3;
308
+ npc.add(nBody, nHead);
309
+ npc.position.set(x, 0, z);
310
+ return npc;
311
+ }
312
+ restGroup.add(createNPC(0, -6, 0x4CAF50)); // Cashier/Customer
313
+ restGroup.add(createNPC(10, 5, 0x9C27B0)); // Customer eating
314
+
315
+ restGroup.userData = { type: 'restaurant' };
316
+ scene.add(restGroup);
317
+
318
+ // --- HOUSES ---
319
+ const buildings = [];
320
+ function buildDetailedHouse(x, z, color) {
321
+ const hGroup = new THREE.Group();
322
+ hGroup.position.set(x, 0, z);
323
+
324
+ // Main structure
325
+ const body = new THREE.Mesh(new THREE.BoxGeometry(10, 8, 10), new THREE.MeshLambertMaterial({map: MATS.brickRed.map, color: color}));
326
+ body.position.y = 4;
327
+ hGroup.add(body);
328
+
329
+ // Roof
330
+ const roof = new THREE.Mesh(new THREE.ConeGeometry(9, 4, 4), MATS.wood);
331
+ roof.position.y = 10;
332
+ roof.rotation.y = Math.PI/4;
333
+ hGroup.add(roof);
334
+
335
+ // Door
336
+ const door = new THREE.Mesh(new THREE.BoxGeometry(2.5, 4.5, 0.5), MATS.wood);
337
+ door.position.set(0, 2.25, 5.1);
338
+ hGroup.add(door);
339
+
340
+ // Door Knob
341
+ const knob = new THREE.Mesh(new THREE.SphereGeometry(0.15), new THREE.MeshBasicMaterial({color:0xFFD700}));
342
+ knob.position.set(0.8, 2.25, 5.4);
343
+ hGroup.add(knob);
344
+
345
+ // Windows
346
+ const winGeo = new THREE.BoxGeometry(2.5, 2.5, 0.5);
347
+ const w1 = new THREE.Mesh(winGeo, MATS.glass); w1.position.set(-2.5, 5, 5.1);
348
+ const w2 = new THREE.Mesh(winGeo, MATS.glass); w2.position.set(2.5, 5, 5.1);
349
+ hGroup.add(w1, w2);
350
+
351
+ // Fence
352
+ const fenceGeo = new THREE.BoxGeometry(0.2, 1.5, 0.2);
353
+ for(let i=-6; i<=6; i+=2) {
354
+ const p = new THREE.Mesh(fenceGeo, new THREE.MeshLambertMaterial({color:0xffffff}));
355
+ p.position.set(i, 0.75, 7);
356
+ hGroup.add(p);
357
+ }
358
+
359
+ hGroup.userData = { type: 'house', deliveryPoint: new THREE.Vector3(x, 1, z+8) };
360
+ buildings.push(hGroup);
361
+ scene.add(hGroup);
362
+ }
363
 
364
+ // Generate Neighborhood
365
+ for(let i=0; i<4; i++) {
366
+ buildDetailedHouse(-40, -40 - (i*25), 0xFFCDD2);
367
+ buildDetailedHouse(40, -40 - (i*25), 0xBBDEFB);
368
+ buildDetailedHouse(-40 - (i*25), 80, 0xFFF9C4);
369
+ }
370
 
371
+ /** --- PLAYER & VEHICLES --- */
372
+ const player = new THREE.Group();
373
+ // Head
374
+ const pHead = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.6, 0.6), MATS.face);
375
+ pHead.position.y = 2.7;
376
+ // Body
377
+ const pTorso = new THREE.Mesh(new THREE.BoxGeometry(0.9, 1.1, 0.45), new THREE.MeshLambertMaterial({color:0x1976D2}));
378
+ pTorso.position.y = 1.85;
379
+ // Arms
380
+ const pArmL = new THREE.Mesh(new THREE.BoxGeometry(0.35, 1.1, 0.35), MATS.skin); pArmL.position.set(-0.7, 1.85, 0);
381
+ const pArmR = new THREE.Mesh(new THREE.BoxGeometry(0.35, 1.1, 0.35), MATS.skin); pArmR.position.set(0.7, 1.85, 0);
382
+ // Legs
383
+ const pLegL = new THREE.Mesh(new THREE.BoxGeometry(0.4, 1.2, 0.4), new THREE.MeshLambertMaterial({color:0x212121})); pLegL.position.set(-0.25, 0.6, 0);
384
+ const pLegR = new THREE.Mesh(new THREE.BoxGeometry(0.4, 1.2, 0.4), new THREE.MeshLambertMaterial({color:0x212121})); pLegR.position.set(0.25, 0.6, 0);
385
+
386
+ // Food Held
387
+ const foodItem = new THREE.Group();
388
+ const burger = new THREE.Mesh(new THREE.CylinderGeometry(0.25, 0.25, 0.15), new THREE.MeshLambertMaterial({color:0x795548}));
389
+ const bunTop = new THREE.Mesh(new THREE.SphereGeometry(0.26, 8, 8, 0, 6.3, 0, 1.5), new THREE.MeshLambertMaterial({color:0xFFA000}));
390
+ bunTop.position.y = 0.1;
391
+ foodItem.add(burger, bunTop);
392
+ foodItem.position.set(0, -0.5, 0.4);
393
+ foodItem.rotation.x = Math.PI/4;
394
+ foodItem.visible = false;
395
+ pArmR.add(foodItem);
396
+
397
+ player.add(pHead, pTorso, pArmL, pArmR, pLegL, pLegR);
398
+ player.position.set(0, 0, 20);
399
+ player.castShadow = true;
400
+ scene.add(player);
401
+
402
+ const worldVehicles = [];
403
+
404
+ function spawnVehicle(data, x, z) {
405
+ const vGroup = new THREE.Group();
406
+ vGroup.position.set(x, 0.5, z);
407
+
408
+ const matBody = new THREE.MeshPhongMaterial({color: data.color, shininess: 80});
409
+ const matDark = new THREE.MeshLambertMaterial({color: 0x222222});
410
+
411
+ if(data.type === 'car') {
412
+ // Detailed Car Body
413
+ const chassis = new THREE.Mesh(new THREE.BoxGeometry(2, 0.8, 4), matBody);
414
+ chassis.position.y = 0.6;
415
+ vGroup.add(chassis);
416
+
417
+ const cabin = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.7, 2.2), matBody);
418
+ cabin.position.set(0, 1.3, -0.3);
419
+ vGroup.add(cabin);
420
+
421
+ // Windows
422
+ const windshield = new THREE.Mesh(new THREE.PlaneGeometry(1.6, 0.6), MATS.glass);
423
+ windshield.position.set(0, 1.35, 0.81); windshield.rotation.x = -0.3;
424
+ vGroup.add(windshield);
425
+
426
+ // Headlights
427
+ const hl1 = new THREE.Mesh(new THREE.CircleGeometry(0.2, 16), new THREE.MeshBasicMaterial({color:0xFFFACD}));
428
+ hl1.position.set(-0.6, 0.6, 2.01); vGroup.add(hl1);
429
+ const hl2 = hl1.clone(); hl2.position.set(0.6, 0.6, 2.01); vGroup.add(hl2);
430
+
431
+ // Wheels (Detailed)
432
+ const wGeo = new THREE.CylinderGeometry(0.4, 0.4, 0.3, 16); wGeo.rotateZ(Math.PI/2);
433
+ const pos = [[1, 0.4, 1.2], [-1, 0.4, 1.2], [1, 0.4, -1.2], [-1, 0.4, -1.2]];
434
+ pos.forEach(p => {
435
+ const w = new THREE.Mesh(wGeo, matDark); w.position.set(...p); vGroup.add(w);
436
+ // Rims
437
+ const rim = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 0.31, 8), new THREE.MeshStandardMaterial({color:0xCCCCCC}));
438
+ rim.rotation.z = Math.PI/2; w.add(rim);
439
+ });
440
+
441
+ } else {
442
+ // Bike
443
+ const body = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.5, 1.5), matBody);
444
+ body.position.y = 0.6; vGroup.add(body);
445
+ // Handle
446
+ const handle = new THREE.Mesh(new THREE.BoxGeometry(1, 0.1, 0.1), matDark);
447
+ handle.position.set(0, 1.2, 0.5); vGroup.add(handle);
448
+ // Wheels
449
+ const wGeo = new THREE.CylinderGeometry(0.4, 0.4, 0.1, 16); wGeo.rotateZ(Math.PI/2);
450
+ const w1 = new THREE.Mesh(wGeo, matDark); w1.position.set(0, 0.4, 0.8); vGroup.add(w1);
451
+ const w2 = new THREE.Mesh(wGeo, matDark); w2.position.set(0, 0.4, -0.8); vGroup.add(w2);
452
+ }
453
+
454
+ vGroup.userData = { isVehicle: true, info: data };
455
+ worldVehicles.push(vGroup);
456
+ scene.add(vGroup);
457
+ return vGroup;
458
+ }
459
 
460
+ // Spawn Initial Vehicle
461
+ spawnVehicle(VEHICLES_DB[2], 5, 15);
462
+
463
+ /** --- INPUTS & LOGIC --- */
464
+ const keys = STATE.keys;
465
+ document.addEventListener('keydown', e => {
466
+ const k = e.key.toLowerCase();
467
+ if(keys[k] !== undefined) keys[k] = true;
468
+ if(e.code === 'Space') jump();
469
+ if(k === 'e') action();
470
+ });
471
+ document.addEventListener('keyup', e => {
472
+ const k = e.key.toLowerCase();
473
+ if(keys[k] !== undefined) keys[k] = false;
474
+ });
475
+
476
+ // Touch
477
+ const bindBtn = (id, key) => {
478
+ const el = document.getElementById(id);
479
+ el.addEventListener('touchstart', (e)=>{e.preventDefault(); keys[key]=true});
480
+ el.addEventListener('touchend', (e)=>{e.preventDefault(); keys[key]=false});
481
+ };
482
+ bindBtn('btn-up','w'); bindBtn('btn-down','s'); bindBtn('btn-left','a'); bindBtn('btn-right','d');
483
+ document.getElementById('btn-jump').addEventListener('touchstart', (e)=>{e.preventDefault(); jump()});
484
+ document.getElementById('context-btn').addEventListener('touchstart', (e)=>{e.preventDefault(); action()});
485
+ document.getElementById('context-btn').addEventListener('click', action);
486
+
487
+ function jump() {
488
+ if(!STATE.inVehicle && STATE.canJump) {
489
+ STATE.velocity.y = 0.5; STATE.canJump = false;
490
+ }
491
+ }
492
 
493
+ function showMsg(txt) {
494
+ const el = document.getElementById('message-area');
495
+ el.innerText = txt; el.style.opacity=1;
496
+ setTimeout(()=>el.style.opacity=0, 2500);
497
+ }
498
 
499
+ function action() {
500
+ // Exit Vehicle
501
+ if(STATE.inVehicle) {
502
+ const v = STATE.inVehicle;
503
+ player.position.copy(v.position).add(new THREE.Vector3(3, 0, 0));
504
+ player.visible = true;
505
+ STATE.inVehicle = null;
506
+ return;
507
+ }
508
+
509
+ // Enter Vehicle
510
+ if(!STATE.inVehicle) {
511
+ let closest = null, minD = 5;
512
+ worldVehicles.forEach(v => {
513
+ if(player.position.distanceTo(v.position) < minD) { minD = player.position.distanceTo(v.position); closest = v; }
514
+ });
515
+ if(closest) {
516
+ STATE.inVehicle = closest;
517
+ player.visible = false;
518
+ showMsg("Driving " + closest.userData.info.name);
519
+ return;
520
+ }
521
+ }
522
+
523
+ // Cooking
524
+ if(player.position.distanceTo(restGroup.position) < 20 && !STATE.holdingFood && !STATE.isCooking) {
525
+ STATE.isCooking = true;
526
+ document.getElementById('cooking-container').style.display = 'block';
527
+ let prog = 0;
528
+ const iv = setInterval(()=>{
529
+ prog += 4;
530
+ document.getElementById('cooking-bar-fill').style.width = prog + '%';
531
+ if(prog>=100) {
532
+ clearInterval(iv);
533
+ STATE.isCooking = false;
534
+ STATE.holdingFood = true;
535
+ foodItem.visible = true;
536
+ document.getElementById('cooking-container').style.display = 'none';
537
+ // Mission
538
+ const h = buildings[Math.floor(Math.random()*buildings.length)];
539
+ STATE.currentOrder = { target: h, reward: 50 };
540
+ document.getElementById('job-display').innerText = "🏠 Deliver Food!";
541
+ showMsg("Order Ready! Find the House!");
542
+ // Marker
543
+ if(window.marker) scene.remove(window.marker);
544
+ window.marker = new THREE.Mesh(new THREE.ConeGeometry(1,2,4), new THREE.MeshBasicMaterial({color:0xFFFF00}));
545
+ window.marker.rotation.x = Math.PI;
546
+ window.marker.position.copy(h.position).add(new THREE.Vector3(0, 12, 0));
547
+ scene.add(window.marker);
548
+ }
549
+ }, 100);
550
+ return;
551
+ }
552
+
553
+ // Deliver
554
+ if(STATE.holdingFood && STATE.currentOrder) {
555
+ if(player.position.distanceTo(STATE.currentOrder.target.userData.deliveryPoint) < 6) {
556
+ STATE.money += STATE.currentOrder.reward;
557
+ document.getElementById('money-display').innerText = STATE.money;
558
+ STATE.holdingFood = false;
559
+ foodItem.visible = false;
560
+ STATE.currentOrder = null;
561
+ document.getElementById('job-display').innerText = "🍔 Return to Restaurant";
562
+ showMsg("Delivered! +$50");
563
+ if(window.marker) { scene.remove(window.marker); window.marker=null; }
564
+ return;
565
+ }
566
+ }
567
+ }
568
+
569
+ // --- SHOP / SUMMON UI ---
570
+ const menu = document.getElementById('vehicle-menu');
571
+ document.getElementById('btn-summon').onclick = () => {
572
+ menu.style.display = 'block';
573
+ const list = document.getElementById('vehicle-list');
574
+ list.innerHTML = '';
575
+ VEHICLES_DB.forEach(v => {
576
+ const div = document.createElement('div');
577
+ div.className = 'v-item';
578
+ div.innerHTML = `<b>${v.name}</b> <button class="v-btn">RIDE</button>`;
579
+ div.querySelector('button').onclick = () => {
580
+ spawnVehicle(v, player.position.x+2, player.position.z);
581
+ menu.style.display = 'none';
582
+ };
583
+ list.appendChild(div);
584
+ });
585
+ };
586
+ document.getElementById('close-menu').onclick = () => menu.style.display='none';
587
+
588
+ /** --- GAME LOOP --- */
589
+ function animate() {
590
+ requestAnimationFrame(animate);
591
+
592
+ // Context Button Text
593
+ let txt = "Action";
594
+ if(STATE.inVehicle) txt = "EXIT";
595
+ else if(player.position.distanceTo(restGroup.position) < 20 && !STATE.holdingFood) txt = "COOK";
596
+ else if(STATE.currentOrder && player.position.distanceTo(STATE.currentOrder.target.userData.deliveryPoint)<6) txt = "DELIVER";
597
+ else {
598
+ worldVehicles.forEach(v => { if(player.position.distanceTo(v.position)<5) txt="DRIVE"; });
599
+ }
600
+ document.getElementById('context-btn').innerText = txt;
601
+
602
+ // Physics
603
+ if(STATE.inVehicle) {
604
+ const v = STATE.inVehicle;
605
+ const speed = v.userData.info.speed * 0.5;
606
+ if(keys.w) v.translateZ(speed);
607
+ if(keys.s) v.translateZ(-speed/2);
608
+ if(keys.a) v.rotation.y += 0.05;
609
+ if(keys.d) v.rotation.y -= 0.05;
610
+
611
+ // Camera
612
+ const off = new THREE.Vector3(0, 6, -10).applyAxisAngle(new THREE.Vector3(0,1,0), v.rotation.y);
613
+ camera.position.lerp(v.position.clone().add(off), 0.1);
614
+ camera.lookAt(v.position);
615
+ player.position.copy(v.position); // Sync hidden player
616
+ } else {
617
+ const speed = 0.3;
618
+ if(keys.w) player.translateZ(speed);
619
+ if(keys.s) player.translateZ(-speed);
620
+ if(keys.a) player.rotation.y += 0.08;
621
+ if(keys.d) player.rotation.y -= 0.08;
622
+
623
+ // Gravity
624
+ if(!STATE.canJump) {
625
+ STATE.velocity.y -= 0.03;
626
+ player.position.y += STATE.velocity.y;
627
+ if(player.position.y <= 0) {
628
+ player.position.y = 0; STATE.canJump = true; STATE.velocity.y = 0;
629
+ }
630
+ }
631
+
632
+ // Camera
633
+ const off = new THREE.Vector3(0, 7, -9).applyAxisAngle(new THREE.Vector3(0,1,0), player.rotation.y);
634
+ camera.position.lerp(player.position.clone().add(off), 0.1);
635
+ camera.lookAt(player.position.clone().add(new THREE.Vector3(0, 3, 0)));
636
+
637
+ // Anim
638
+ if(keys.w || keys.s) {
639
+ const t = Date.now() * 0.01;
640
+ pLegL.rotation.x = Math.sin(t)*0.5;
641
+ pLegR.rotation.x = -Math.sin(t)*0.5;
642
+ pArmL.rotation.x = -Math.sin(t)*0.5;
643
+ pArmR.rotation.x = Math.sin(t)*0.5;
644
+ }
645
+ }
646
+
647
+ if(window.marker) window.marker.rotation.y += 0.05;
648
+ renderer.render(scene, camera);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
  }
650
+ animate();
651
+
652
+ window.onresize = () => {
653
+ camera.aspect = window.innerWidth/window.innerHeight;
654
+ camera.updateProjectionMatrix();
655
+ renderer.setSize(window.innerWidth, window.innerHeight);
656
+ };
657
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  </body>
659
+ </html>