| Server IP : 104.21.21.239 / Your IP : 216.73.217.143 Web Server : Apache/2.4.68 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-69-123.ec2.internal 6.1.176-223.369.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Fri Jul 24 13:34:27 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/home2/cgny/public_html/2019/events-chat/ |
Upload File : |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>City Guide NY Events Chat</title>
<style>
:root {
--ink: #1a1714;
--muted: #6a6258;
--paper: #f7f1e8;
--panel: #fffdf9;
--line: #e2d6c6;
--accent: #0b4f6c;
--accent-2: #c45c26;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
color: var(--ink);
background:
radial-gradient(1200px 500px at 10% -10%, #ffe8d2 0%, transparent 55%),
radial-gradient(900px 400px at 100% 0%, #d9eef5 0%, transparent 50%),
linear-gradient(180deg, #f3ebe0, #f8f4ee 40%, #efe7db);
}
.wrap {
max-width: 760px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
padding: 20px 16px 16px;
}
header h1 {
margin: 0;
font-size: clamp(1.6rem, 4vw, 2.2rem);
letter-spacing: -0.02em;
line-height: 1.15;
}
header p {
margin: 8px 0 0;
color: var(--muted);
font-size: 1rem;
max-width: 38rem;
}
.meta {
margin-top: 10px;
font-size: 0.85rem;
color: var(--muted);
}
#thread {
flex: 1;
overflow-y: auto;
margin: 18px 0;
padding: 8px 2px;
display: flex;
flex-direction: column;
gap: 14px;
}
.msg {
max-width: 92%;
padding: 12px 14px;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: 0 8px 24px rgba(40, 24, 8, 0.04);
white-space: pre-wrap;
line-height: 1.45;
}
.msg.user {
align-self: flex-end;
background: #12384a;
color: #f7fbff;
border-color: #0b4f6c;
}
.msg.bot { align-self: flex-start; }
.msg .cost {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--line);
font-size: 0.78rem;
color: var(--muted);
white-space: normal;
}
.msg a { color: var(--accent); }
.msg.user a { color: #b9e4ff; }
form {
display: flex;
gap: 8px;
align-items: flex-end;
background: rgba(255,253,249,0.9);
border: 1px solid var(--line);
padding: 10px;
}
textarea {
flex: 1;
min-height: 54px;
max-height: 160px;
resize: vertical;
border: 0;
outline: none;
background: transparent;
font: inherit;
padding: 8px;
}
button {
border: 0;
background: var(--accent-2);
color: #fff;
font: inherit;
padding: 12px 16px;
cursor: pointer;
}
button:disabled { opacity: 0.6; cursor: wait; }
.toolbar {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
margin-top: 8px;
font-size: 0.85rem;
color: var(--muted);
}
.toolbar button {
background: transparent;
color: var(--accent);
border: 1px solid var(--line);
padding: 6px 10px;
}
.empty {
color: var(--muted);
font-style: italic;
padding: 24px 8px;
}
.err {
color: #8a1f11;
background: #ffe8e2;
border: 1px solid #f0c2b8;
padding: 10px 12px;
margin-bottom: 8px;
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>City Guide NY Events Chat</h1>
<p>Ask about upcoming NYC events by category, date, neighborhood, price, or free options. Answers use an hourly-updated events cache.</p>
<div class="meta" id="meta">Loading event cache…</div>
</header>
<div id="thread">
<div class="empty" id="empty">Try: “Free outdoor events this weekend” or “Family-friendly museums next week.”</div>
</div>
<div id="error" class="err" hidden></div>
<form id="chat-form">
<textarea id="message" placeholder="Ask about upcoming events…" required maxlength="2000"></textarea>
<button type="submit" id="send">Ask</button>
</form>
<div class="toolbar">
<span id="running-total">Conversation cost: $0.0000</span>
<button type="button" id="new-chat">New chat</button>
</div>
</div>
<script>
const state = {
conversationId: null,
sessionToken: null,
conversationCost: 0
};
const thread = document.getElementById('thread');
const form = document.getElementById('chat-form');
const input = document.getElementById('message');
const sendBtn = document.getElementById('send');
const errorBox = document.getElementById('error');
const meta = document.getElementById('meta');
const runningTotal = document.getElementById('running-total');
const emptyPrompt = 'Try: “Free outdoor events this weekend” or “Family-friendly museums next week.”';
function showError(msg) {
errorBox.hidden = !msg;
errorBox.textContent = msg || '';
}
function formatTokens(n) {
if (n >= 1000) return (n / 1000).toFixed(n >= 10000 ? 0 : 1) + 'k';
return String(n);
}
function linkify(text) {
const escaped = text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
return escaped.replace(
/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,
'<a href="$2" target="_blank" rel="noopener">$1</a>'
).replace(
/(https?:\/\/www\.cityguideny\.com\/event\/[a-z0-9_-]+)/gi,
'<a href="$1" target="_blank" rel="noopener">$1</a>'
);
}
function clearEmpty() {
const empty = document.getElementById('empty');
if (empty) empty.remove();
}
function appendUserMessage(text) {
clearEmpty();
const el = document.createElement('div');
el.className = 'msg user';
el.innerHTML = linkify(text);
thread.appendChild(el);
thread.scrollTop = thread.scrollHeight;
}
function createBotMessage() {
clearEmpty();
const el = document.createElement('div');
el.className = 'msg bot';
const body = document.createElement('div');
body.className = 'body';
body.textContent = '';
const cost = document.createElement('div');
cost.className = 'cost';
cost.hidden = true;
el.appendChild(body);
el.appendChild(cost);
thread.appendChild(el);
thread.scrollTop = thread.scrollHeight;
return { el, body, cost };
}
function updateBotText(bot, text) {
bot.body.innerHTML = linkify(text);
thread.scrollTop = thread.scrollHeight;
}
function finishBot(bot, usage) {
if (!usage) return;
const cached = usage.cached_tokens || 0;
bot.cost.hidden = false;
bot.cost.textContent = '~' + (usage.cost_display || '$0') +
' · ' + formatTokens(usage.total_tokens || 0) + ' tokens' +
(cached ? ' (' + formatTokens(cached) + ' cached)' : '') +
(usage.latency_ms ? ' · ' + usage.latency_ms + ' ms' : '');
}
function resetChat() {
state.conversationId = null;
state.sessionToken = null;
state.conversationCost = 0;
thread.innerHTML = '<div class="empty" id="empty">' + emptyPrompt + '</div>';
runningTotal.textContent = 'Conversation cost: $0.0000';
showError('');
input.focus();
}
async function readSse(response, handlers) {
if (!response.ok) {
let msg = 'Request failed';
try {
const data = await response.json();
if (data && data.error) msg = data.error;
} catch (_) {}
throw new Error(msg);
}
if (!response.body) throw new Error('Streaming not supported in this browser.');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let eventName = 'message';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
buffer = buffer.replace(/\r\n/g, '\n');
let sep;
while ((sep = buffer.indexOf('\n\n')) !== -1) {
const rawEvent = buffer.slice(0, sep);
buffer = buffer.slice(sep + 2);
const lines = rawEvent.split('\n');
eventName = 'message';
const dataLines = [];
for (const line of lines) {
if (line.startsWith('event:')) eventName = line.slice(6).trim();
else if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart());
}
if (!dataLines.length) continue;
let payload = {};
try { payload = JSON.parse(dataLines.join('\n')); } catch (_) { continue; }
if (handlers[eventName]) handlers[eventName](payload);
}
}
}
document.getElementById('new-chat').addEventListener('click', resetChat);
form.addEventListener('submit', async (e) => {
e.preventDefault();
const message = input.value.trim();
if (!message) return;
showError('');
appendUserMessage(message);
input.value = '';
sendBtn.disabled = true;
const bot = createBotMessage();
let full = '';
try {
const res = await fetch('api/chat.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
},
body: JSON.stringify({
message,
conversation_id: state.conversationId,
session_token: state.sessionToken
})
});
await readSse(res, {
meta(data) {
state.conversationId = data.conversation_id;
state.sessionToken = data.session_token;
if (data.event_count) {
meta.textContent = data.event_count + ' upcoming events cached · model gpt-4o-mini · cache ' + (data.cache_version || '');
}
},
delta(data) {
full += (data.text || '');
updateBotText(bot, full);
},
done(data) {
state.conversationCost = data.conversation_cost_usd || 0;
runningTotal.textContent = 'Conversation cost: ' + (data.conversation_cost_display || '$0');
finishBot(bot, data.usage);
},
error(data) {
throw new Error(data.error || 'Stream error');
}
});
if (!full) updateBotText(bot, 'Sorry, I could not generate a response.');
} catch (err) {
showError(err.message || 'Something went wrong');
if (!full) updateBotText(bot, 'Something went wrong while generating a reply.');
} finally {
sendBtn.disabled = false;
input.focus();
}
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
form.requestSubmit();
}
});
meta.textContent = 'Ask a question to load the live events cache.';
</script>
</body>
</html>