mirror of
https://github.com/multipleof4/sune.git
synced 2026-01-13 16:17:55 +00:00
123
docs/index.html
123
docs/index.html
@@ -5,10 +5,8 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<title>ChatGPT — Mobile Clone (Light)</title>
|
<title>ChatGPT — Mobile Clone (Light)</title>
|
||||||
<!-- Tailwind CSS CDN -->
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<style>
|
<style>
|
||||||
/* iOS safe area for the input dock */
|
|
||||||
:root { --safe-bottom: env(safe-area-inset-bottom); }
|
:root { --safe-bottom: env(safe-area-inset-bottom); }
|
||||||
::-webkit-scrollbar { height: 8px; width: 8px; }
|
::-webkit-scrollbar { height: 8px; width: 8px; }
|
||||||
::-webkit-scrollbar-thumb { background: #e5e7eb; border-radius: 999px; }
|
::-webkit-scrollbar-thumb { background: #e5e7eb; border-radius: 999px; }
|
||||||
@@ -17,25 +15,19 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white text-gray-900 selection:bg-amber-200/60">
|
<body class="bg-white text-gray-900 selection:bg-amber-200/60">
|
||||||
<!-- App Shell -->
|
|
||||||
<div class="flex flex-col h-dvh max-h-dvh">
|
<div class="flex flex-col h-dvh max-h-dvh">
|
||||||
<!-- Header -->
|
|
||||||
<header class="sticky top-0 z-10 bg-white/80 backdrop-blur border-b border-gray-200">
|
<header class="sticky top-0 z-10 bg-white/80 backdrop-blur border-b border-gray-200">
|
||||||
<div class="mx-auto w-full max-w-2xl px-4 py-3 flex items-center justify-between">
|
<div class="mx-auto w-full max-w-2xl px-4 py-3 flex items-center justify-between">
|
||||||
<button id="newChatBtn" class="text-sm font-medium px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition">
|
<button id="newChatBtn" class="text-sm font-medium px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition">New chat</button>
|
||||||
New chat
|
<button id="modelBadge" class="text-xs font-medium px-3 py-1.5 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100 transition">
|
||||||
|
<span id="modelName">openai/gpt-4o</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="text-sm text-gray-600 truncate" id="modelBadge">
|
|
||||||
Model: <span class="font-semibold" id="modelName">openai/gpt-4o</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-[11px] text-gray-400" id="status">offline</span>
|
<span class="text-[11px] text-gray-400" id="status">offline</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header><!-- Messages -->
|
</header><main id="chat" class="flex-1 overflow-y-auto no-scrollbar">
|
||||||
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar">
|
|
||||||
<div id="messages" class="mx-auto w-full max-w-2xl px-4 py-4 sm:py-6 space-y-4">
|
<div id="messages" class="mx-auto w-full max-w-2xl px-4 py-4 sm:py-6 space-y-4">
|
||||||
<!-- Placeholder intro -->
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<div class="shrink-0 h-8 w-8 rounded-full bg-emerald-100 text-emerald-700 flex items-center justify-center">🤖</div>
|
<div class="shrink-0 h-8 w-8 rounded-full bg-emerald-100 text-emerald-700 flex items-center justify-center">🤖</div>
|
||||||
<div class="bg-gray-100 rounded-2xl px-4 py-3 text-[15px] leading-relaxed">
|
<div class="bg-gray-100 rounded-2xl px-4 py-3 text-[15px] leading-relaxed">
|
||||||
@@ -44,54 +36,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-24"></div> <!-- spacer so last message isn't hidden under input dock -->
|
<div class="h-24"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Input Dock -->
|
|
||||||
<footer class="sticky bottom-0 bg-gradient-to-t from-white via-white/95 to-white/40 pt-3 pb-[calc(12px+var(--safe-bottom))] border-t border-gray-200">
|
<footer class="sticky bottom-0 bg-gradient-to-t from-white via-white/95 to-white/40 pt-3 pb-[calc(12px+var(--safe-bottom))] border-t border-gray-200">
|
||||||
<div class="mx-auto w-full max-w-2xl px-4">
|
<div class="mx-auto w-full max-w-2xl px-4">
|
||||||
<form id="composer" class="group relative flex items-end gap-2">
|
<form id="composer" class="group relative flex items-end gap-2">
|
||||||
<textarea id="input" rows="1" placeholder="Send a message" spellcheck="true"
|
<textarea id="input" rows="1" placeholder="Send a message" spellcheck="true" class="flex-1 resize-none rounded-2xl border border-gray-300 bg-white px-4 py-3 text-[15px] leading-6 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-amber-300/80 focus:border-gray-300 max-h-40"></textarea>
|
||||||
class="flex-1 resize-none rounded-2xl border border-gray-300 bg-white px-4 py-3 text-[15px] leading-6 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-amber-300/80 focus:border-gray-300 max-h-40"></textarea>
|
<button id="sendBtn" type="submit" aria-label="Send" class="shrink-0 rounded-2xl bg-amber-500 text-white h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-amber-600 active:scale-[.98] transition disabled:opacity-40 disabled:cursor-not-allowed">
|
||||||
<button id="sendBtn" type="submit" aria-label="Send"
|
<svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||||
class="shrink-0 rounded-2xl bg-amber-500 text-white h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-amber-600 active:scale-[.98] transition disabled:opacity-40 disabled:cursor-not-allowed">
|
|
||||||
<!-- Send icon -->
|
|
||||||
<svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M12 5l7 7-7 7"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="mt-2 flex items-center justify-between text-xs text-gray-500">
|
<div class="mt-2 flex items-center justify-between text-xs text-gray-500">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Enter</kbd>
|
<kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Enter</kbd> to send • <kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Shift</kbd>+<kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Enter</kbd> for newline
|
||||||
to send • <kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Shift</kbd>+<kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Enter</kbd> for newline
|
|
||||||
</div>
|
</div>
|
||||||
<button id="clearBtn" class="underline decoration-dotted hover:text-gray-700">Clear</button>
|
<button id="clearBtn" class="underline decoration-dotted hover:text-gray-700">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Corner Controls (bottom-left) -->
|
|
||||||
<div class="fixed bottom-[calc(68px+var(--safe-bottom))] left-3 flex flex-col gap-2">
|
<div class="fixed bottom-[calc(68px+var(--safe-bottom))] left-3 flex flex-col gap-2">
|
||||||
<button id="setApiBtn" title="Set OpenRouter API key" class="h-10 w-10 rounded-full bg-gray-100 border border-gray-200 shadow-sm hover:bg-gray-200 active:scale-[.98] transition flex items-center justify-center">
|
<button id="setApiBtn" title="Set OpenRouter API key" class="h-10 w-10 rounded-full bg-gray-100 border border-gray-200 shadow-sm hover:bg-gray-200 active:scale-[.98] transition flex items-center justify-center">
|
||||||
<!-- User icon -->
|
|
||||||
<svg viewBox="0 0 24 24" class="h-5 w-5 text-gray-700" fill="none" stroke="currentColor" stroke-width="1.6">
|
<svg viewBox="0 0 24 24" class="h-5 w-5 text-gray-700" fill="none" stroke="currentColor" stroke-width="1.6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6.75a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6.75a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"/>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 20.25a8.25 8.25 0 0115 0"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 20.25a8.25 8.25 0 0115 0"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="setModelBtn" title="Set model" class="h-10 w-10 rounded-full bg-emerald-50 border border-emerald-200 shadow-sm hover:bg-emerald-100 active:scale-[.98] transition flex items-center justify-center">
|
|
||||||
<!-- Bot icon -->
|
|
||||||
<svg viewBox="0 0 24 24" class="h-5 w-5 text-emerald-700" fill="none" stroke="currentColor" stroke-width="1.6">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 7h6M4 11h16v4a5 5 0 01-5 5H9a5 5 0 01-5-5v-4z"/>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 3h6v4H9z"/>
|
|
||||||
<circle cx="9" cy="14" r="1"/><circle cx="15" cy="14" r="1"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div> <script>
|
</div> <script>
|
||||||
// === State ===
|
|
||||||
const el = {
|
const el = {
|
||||||
chat: document.getElementById('chat'),
|
chat: document.getElementById('chat'),
|
||||||
messages: document.getElementById('messages'),
|
messages: document.getElementById('messages'),
|
||||||
@@ -104,7 +78,6 @@
|
|||||||
modelName: document.getElementById('modelName'),
|
modelName: document.getElementById('modelName'),
|
||||||
modelBadge: document.getElementById('modelBadge'),
|
modelBadge: document.getElementById('modelBadge'),
|
||||||
setApiBtn: document.getElementById('setApiBtn'),
|
setApiBtn: document.getElementById('setApiBtn'),
|
||||||
setModelBtn: document.getElementById('setModelBtn'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = {
|
const store = {
|
||||||
@@ -114,35 +87,22 @@
|
|||||||
set model(v) { localStorage.setItem('openrouter_model', v || 'openai/gpt-4o'); el.modelName.textContent = store.model; },
|
set model(v) { localStorage.setItem('openrouter_model', v || 'openai/gpt-4o'); el.modelName.textContent = store.model; },
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = { messages: [], busy: false, controller: null };
|
||||||
messages: [], // {role: 'user'|'assistant'|'system', content: string}
|
|
||||||
busy: false,
|
|
||||||
controller: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// === UI Helpers ===
|
|
||||||
function addMessage(role, content) {
|
function addMessage(role, content) {
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'flex gap-3';
|
row.className = 'flex gap-3';
|
||||||
|
|
||||||
const avatar = document.createElement('div');
|
const avatar = document.createElement('div');
|
||||||
avatar.className = 'shrink-0 h-8 w-8 rounded-full flex items-center justify-center ' + (role === 'user' ? 'bg-blue-100 text-blue-700' : 'bg-emerald-100 text-emerald-700');
|
avatar.className = 'shrink-0 h-8 w-8 rounded-full flex items-center justify-center ' + (role === 'user' ? 'bg-blue-100 text-blue-700' : 'bg-emerald-100 text-emerald-700');
|
||||||
avatar.textContent = role === 'user' ? '🧑' : '🤖';
|
avatar.textContent = role === 'user' ? '🧑' : '🤖';
|
||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = 'rounded-2xl px-4 py-3 text-[15px] leading-relaxed whitespace-pre-wrap ' + (role === 'user' ? 'bg-blue-50 border border-blue-100' : 'bg-gray-100');
|
bubble.className = 'rounded-2xl px-4 py-3 text-[15px] leading-relaxed whitespace-pre-wrap ' + (role === 'user' ? 'bg-blue-50 border border-blue-100' : 'bg-gray-100');
|
||||||
bubble.textContent = content;
|
bubble.textContent = content;
|
||||||
|
|
||||||
row.appendChild(avatar);
|
row.appendChild(avatar);
|
||||||
row.appendChild(bubble);
|
row.appendChild(bubble);
|
||||||
el.messages.appendChild(row);
|
el.messages.appendChild(row);
|
||||||
|
|
||||||
// Keep internal transcript (for API calls)
|
|
||||||
state.messages.push({ role, content });
|
state.messages.push({ role, content });
|
||||||
|
|
||||||
// Auto-scroll
|
|
||||||
queueMicrotask(() => el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: 'smooth' }));
|
queueMicrotask(() => el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: 'smooth' }));
|
||||||
|
|
||||||
return bubble;
|
return bubble;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,12 +128,7 @@
|
|||||||
if (intro) {
|
if (intro) {
|
||||||
const introRow = document.createElement('div');
|
const introRow = document.createElement('div');
|
||||||
introRow.className = 'flex gap-3';
|
introRow.className = 'flex gap-3';
|
||||||
introRow.innerHTML = `
|
introRow.innerHTML = `<div class="shrink-0 h-8 w-8 rounded-full bg-emerald-100 text-emerald-700 flex items-center justify-center">🤖</div><div class="bg-gray-100 rounded-2xl px-4 py-3 text-[15px] leading-relaxed"><div class="font-semibold text-gray-800 mb-1">New chat</div>You're in a fresh conversation. Ask away.</div>`;
|
||||||
<div class="shrink-0 h-8 w-8 rounded-full bg-emerald-100 text-emerald-700 flex items-center justify-center">🤖</div>
|
|
||||||
<div class="bg-gray-100 rounded-2xl px-4 py-3 text-[15px] leading-relaxed">
|
|
||||||
<div class="font-semibold text-gray-800 mb-1">New chat</div>
|
|
||||||
You're in a fresh conversation. Ask away.
|
|
||||||
</div>`;
|
|
||||||
el.messages.appendChild(introRow);
|
el.messages.appendChild(introRow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,37 +139,21 @@
|
|||||||
el.status.className = 'text-[11px] ' + (online ? 'text-emerald-600' : 'text-gray-400');
|
el.status.className = 'text-[11px] ' + (online ? 'text-emerald-600' : 'text-gray-400');
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Comms ===
|
|
||||||
async function askOpenRouter() {
|
async function askOpenRouter() {
|
||||||
const apiKey = store.apiKey;
|
const apiKey = store.apiKey;
|
||||||
const model = store.model;
|
const model = store.model;
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
// Local demo reply
|
return { ok: true, text: localDemoReply(state.messages[state.messages.length - 1]?.content || '') };
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
text: localDemoReply(state.messages[state.messages.length - 1]?.content || '')
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
state.controller = new AbortController();
|
state.controller = new AbortController();
|
||||||
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify({ model, messages: state.messages.filter(m => m.role !== 'system'), stream: false }),
|
||||||
'Authorization': 'Bearer ' + apiKey,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model,
|
|
||||||
messages: state.messages.filter(m => m.role !== 'system'),
|
|
||||||
stream: false,
|
|
||||||
}),
|
|
||||||
signal: state.controller.signal,
|
signal: state.controller.signal,
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) throw new Error(await res.text() || ('HTTP ' + res.status));
|
||||||
const err = await res.text();
|
|
||||||
throw new Error(err || ('HTTP ' + res.status));
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const choice = data.choices?.[0]?.message?.content?.trim() || '(no content)';
|
const choice = data.choices?.[0]?.message?.content?.trim() || '(no content)';
|
||||||
return { ok: true, text: choice };
|
return { ok: true, text: choice };
|
||||||
@@ -227,18 +166,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function localDemoReply(prompt) {
|
function localDemoReply(prompt) {
|
||||||
// Very tiny offline echo with a dash of personality
|
const tips = [ 'Tip: set an OpenRouter API key (bottom-left) to go truly online.', 'Pro move: click the model badge to change model.', 'New chats are stateless here—no history is kept.' ];
|
||||||
const tips = [
|
|
||||||
'Tip: set an OpenRouter API key (bottom-left) to go truly online.',
|
|
||||||
'Pro move: tap the bot icon to pick a different model.',
|
|
||||||
'New chats are stateless here—no history is kept.',
|
|
||||||
];
|
|
||||||
const tip = tips[Math.floor(Math.random() * tips.length)];
|
const tip = tips[Math.floor(Math.random() * tips.length)];
|
||||||
const mirrored = prompt.split(/\s+/).slice(0, 24).join(' ');
|
const mirrored = prompt.split(/\s+/).slice(0, 24).join(' ');
|
||||||
return `Local demo mode. You said: "${mirrored}"\n\n${tip}`;
|
return `Local demo mode. You said: "${mirrored}"\n\n${tip}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Events ===
|
|
||||||
el.composer.addEventListener('submit', async (e) => {
|
el.composer.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (state.busy) return;
|
if (state.busy) return;
|
||||||
@@ -246,32 +179,25 @@
|
|||||||
if (!text) return;
|
if (!text) return;
|
||||||
el.input.value = '';
|
el.input.value = '';
|
||||||
el.input.style.height = 'auto';
|
el.input.style.height = 'auto';
|
||||||
|
|
||||||
addMessage('user', text);
|
addMessage('user', text);
|
||||||
state.busy = true;
|
state.busy = true;
|
||||||
el.sendBtn.disabled = true;
|
el.sendBtn.disabled = true;
|
||||||
const thinking = addThinkingBubble();
|
const thinking = addThinkingBubble();
|
||||||
|
|
||||||
const res = await askOpenRouter();
|
const res = await askOpenRouter();
|
||||||
thinking.textContent = res.text;
|
thinking.textContent = res.text;
|
||||||
|
|
||||||
// Replace thinking bubble with final assistant bubble style
|
|
||||||
thinking.className = 'rounded-2xl px-4 py-3 text-[15px] leading-relaxed bg-gray-100 text-gray-800 whitespace-pre-wrap';
|
thinking.className = 'rounded-2xl px-4 py-3 text-[15px] leading-relaxed bg-gray-100 text-gray-800 whitespace-pre-wrap';
|
||||||
state.messages[state.messages.length - 1] = { role: 'assistant', content: res.text };
|
state.messages[state.messages.length - 1] = { role: 'assistant', content: res.text };
|
||||||
|
|
||||||
el.sendBtn.disabled = false;
|
el.sendBtn.disabled = false;
|
||||||
state.busy = false;
|
state.busy = false;
|
||||||
queueMicrotask(() => el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: 'smooth' }));
|
queueMicrotask(() => el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: 'smooth' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-resize textarea
|
|
||||||
el.input.addEventListener('input', () => {
|
el.input.addEventListener('input', () => {
|
||||||
el.input.style.height = 'auto';
|
el.input.style.height = 'auto';
|
||||||
const h = Math.min(el.input.scrollHeight, 160); // cap at 160px
|
const h = Math.min(el.input.scrollHeight, 160);
|
||||||
el.input.style.height = h + 'px';
|
el.input.style.height = h + 'px';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enter to send, Shift+Enter for new line
|
|
||||||
el.input.addEventListener('keydown', (e) => {
|
el.input.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -279,30 +205,21 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear chat
|
|
||||||
el.clearBtn.addEventListener('click', () => clearChat(false));
|
el.clearBtn.addEventListener('click', () => clearChat(false));
|
||||||
|
|
||||||
// New chat (fresh intro)
|
|
||||||
el.newChatBtn.addEventListener('click', () => clearChat(true));
|
el.newChatBtn.addEventListener('click', () => clearChat(true));
|
||||||
|
|
||||||
// Set API key
|
|
||||||
el.setApiBtn.addEventListener('click', () => {
|
el.setApiBtn.addEventListener('click', () => {
|
||||||
const current = store.apiKey ? '********' : '';
|
const current = store.apiKey ? '********' : '';
|
||||||
const input = prompt('Enter OpenRouter API key (stored locally):', current);
|
const input = prompt('Enter OpenRouter API key (stored locally):', current);
|
||||||
if (input === null) return; // cancel
|
if (input === null) return;
|
||||||
store.apiKey = input === '********' ? store.apiKey : (input.trim());
|
store.apiKey = input === '********' ? store.apiKey : (input.trim());
|
||||||
alert(store.apiKey ? 'API key saved locally.' : 'API key cleared.');
|
alert(store.apiKey ? 'API key saved locally.' : 'API key cleared.');
|
||||||
});
|
});
|
||||||
|
el.modelBadge.addEventListener('click', () => {
|
||||||
// Set Model
|
|
||||||
el.setModelBtn.addEventListener('click', () => {
|
|
||||||
const input = prompt('Enter model name (e.g., openai/gpt-4o):', store.model);
|
const input = prompt('Enter model name (e.g., openai/gpt-4o):', store.model);
|
||||||
if (input === null) return;
|
if (input === null) return;
|
||||||
const name = input.trim() || 'openai/gpt-4o';
|
store.model = input.trim() || 'openai/gpt-4o';
|
||||||
store.model = name;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Init
|
|
||||||
updateStatus();
|
updateStatus();
|
||||||
el.modelName.textContent = store.model;
|
el.modelName.textContent = store.model;
|
||||||
</script></body>
|
</script></body>
|
||||||
|
|||||||
123
index.html
123
index.html
@@ -5,10 +5,8 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<title>ChatGPT — Mobile Clone (Light)</title>
|
<title>ChatGPT — Mobile Clone (Light)</title>
|
||||||
<!-- Tailwind CSS CDN -->
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<style>
|
<style>
|
||||||
/* iOS safe area for the input dock */
|
|
||||||
:root { --safe-bottom: env(safe-area-inset-bottom); }
|
:root { --safe-bottom: env(safe-area-inset-bottom); }
|
||||||
::-webkit-scrollbar { height: 8px; width: 8px; }
|
::-webkit-scrollbar { height: 8px; width: 8px; }
|
||||||
::-webkit-scrollbar-thumb { background: #e5e7eb; border-radius: 999px; }
|
::-webkit-scrollbar-thumb { background: #e5e7eb; border-radius: 999px; }
|
||||||
@@ -17,25 +15,19 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white text-gray-900 selection:bg-amber-200/60">
|
<body class="bg-white text-gray-900 selection:bg-amber-200/60">
|
||||||
<!-- App Shell -->
|
|
||||||
<div class="flex flex-col h-dvh max-h-dvh">
|
<div class="flex flex-col h-dvh max-h-dvh">
|
||||||
<!-- Header -->
|
|
||||||
<header class="sticky top-0 z-10 bg-white/80 backdrop-blur border-b border-gray-200">
|
<header class="sticky top-0 z-10 bg-white/80 backdrop-blur border-b border-gray-200">
|
||||||
<div class="mx-auto w-full max-w-2xl px-4 py-3 flex items-center justify-between">
|
<div class="mx-auto w-full max-w-2xl px-4 py-3 flex items-center justify-between">
|
||||||
<button id="newChatBtn" class="text-sm font-medium px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition">
|
<button id="newChatBtn" class="text-sm font-medium px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition">New chat</button>
|
||||||
New chat
|
<button id="modelBadge" class="text-xs font-medium px-3 py-1.5 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100 transition">
|
||||||
|
<span id="modelName">openai/gpt-4o</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="text-sm text-gray-600 truncate" id="modelBadge">
|
|
||||||
Model: <span class="font-semibold" id="modelName">openai/gpt-4o</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-[11px] text-gray-400" id="status">offline</span>
|
<span class="text-[11px] text-gray-400" id="status">offline</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header><!-- Messages -->
|
</header><main id="chat" class="flex-1 overflow-y-auto no-scrollbar">
|
||||||
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar">
|
|
||||||
<div id="messages" class="mx-auto w-full max-w-2xl px-4 py-4 sm:py-6 space-y-4">
|
<div id="messages" class="mx-auto w-full max-w-2xl px-4 py-4 sm:py-6 space-y-4">
|
||||||
<!-- Placeholder intro -->
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<div class="shrink-0 h-8 w-8 rounded-full bg-emerald-100 text-emerald-700 flex items-center justify-center">🤖</div>
|
<div class="shrink-0 h-8 w-8 rounded-full bg-emerald-100 text-emerald-700 flex items-center justify-center">🤖</div>
|
||||||
<div class="bg-gray-100 rounded-2xl px-4 py-3 text-[15px] leading-relaxed">
|
<div class="bg-gray-100 rounded-2xl px-4 py-3 text-[15px] leading-relaxed">
|
||||||
@@ -44,54 +36,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-24"></div> <!-- spacer so last message isn't hidden under input dock -->
|
<div class="h-24"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Input Dock -->
|
|
||||||
<footer class="sticky bottom-0 bg-gradient-to-t from-white via-white/95 to-white/40 pt-3 pb-[calc(12px+var(--safe-bottom))] border-t border-gray-200">
|
<footer class="sticky bottom-0 bg-gradient-to-t from-white via-white/95 to-white/40 pt-3 pb-[calc(12px+var(--safe-bottom))] border-t border-gray-200">
|
||||||
<div class="mx-auto w-full max-w-2xl px-4">
|
<div class="mx-auto w-full max-w-2xl px-4">
|
||||||
<form id="composer" class="group relative flex items-end gap-2">
|
<form id="composer" class="group relative flex items-end gap-2">
|
||||||
<textarea id="input" rows="1" placeholder="Send a message" spellcheck="true"
|
<textarea id="input" rows="1" placeholder="Send a message" spellcheck="true" class="flex-1 resize-none rounded-2xl border border-gray-300 bg-white px-4 py-3 text-[15px] leading-6 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-amber-300/80 focus:border-gray-300 max-h-40"></textarea>
|
||||||
class="flex-1 resize-none rounded-2xl border border-gray-300 bg-white px-4 py-3 text-[15px] leading-6 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-amber-300/80 focus:border-gray-300 max-h-40"></textarea>
|
<button id="sendBtn" type="submit" aria-label="Send" class="shrink-0 rounded-2xl bg-amber-500 text-white h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-amber-600 active:scale-[.98] transition disabled:opacity-40 disabled:cursor-not-allowed">
|
||||||
<button id="sendBtn" type="submit" aria-label="Send"
|
<svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||||
class="shrink-0 rounded-2xl bg-amber-500 text-white h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-amber-600 active:scale-[.98] transition disabled:opacity-40 disabled:cursor-not-allowed">
|
|
||||||
<!-- Send icon -->
|
|
||||||
<svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M12 5l7 7-7 7"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="mt-2 flex items-center justify-between text-xs text-gray-500">
|
<div class="mt-2 flex items-center justify-between text-xs text-gray-500">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Enter</kbd>
|
<kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Enter</kbd> to send • <kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Shift</kbd>+<kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Enter</kbd> for newline
|
||||||
to send • <kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Shift</kbd>+<kbd class="px-1.5 py-0.5 rounded border bg-gray-50">Enter</kbd> for newline
|
|
||||||
</div>
|
</div>
|
||||||
<button id="clearBtn" class="underline decoration-dotted hover:text-gray-700">Clear</button>
|
<button id="clearBtn" class="underline decoration-dotted hover:text-gray-700">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Corner Controls (bottom-left) -->
|
|
||||||
<div class="fixed bottom-[calc(68px+var(--safe-bottom))] left-3 flex flex-col gap-2">
|
<div class="fixed bottom-[calc(68px+var(--safe-bottom))] left-3 flex flex-col gap-2">
|
||||||
<button id="setApiBtn" title="Set OpenRouter API key" class="h-10 w-10 rounded-full bg-gray-100 border border-gray-200 shadow-sm hover:bg-gray-200 active:scale-[.98] transition flex items-center justify-center">
|
<button id="setApiBtn" title="Set OpenRouter API key" class="h-10 w-10 rounded-full bg-gray-100 border border-gray-200 shadow-sm hover:bg-gray-200 active:scale-[.98] transition flex items-center justify-center">
|
||||||
<!-- User icon -->
|
|
||||||
<svg viewBox="0 0 24 24" class="h-5 w-5 text-gray-700" fill="none" stroke="currentColor" stroke-width="1.6">
|
<svg viewBox="0 0 24 24" class="h-5 w-5 text-gray-700" fill="none" stroke="currentColor" stroke-width="1.6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6.75a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6.75a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"/>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 20.25a8.25 8.25 0 0115 0"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 20.25a8.25 8.25 0 0115 0"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="setModelBtn" title="Set model" class="h-10 w-10 rounded-full bg-emerald-50 border border-emerald-200 shadow-sm hover:bg-emerald-100 active:scale-[.98] transition flex items-center justify-center">
|
|
||||||
<!-- Bot icon -->
|
|
||||||
<svg viewBox="0 0 24 24" class="h-5 w-5 text-emerald-700" fill="none" stroke="currentColor" stroke-width="1.6">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 7h6M4 11h16v4a5 5 0 01-5 5H9a5 5 0 01-5-5v-4z"/>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 3h6v4H9z"/>
|
|
||||||
<circle cx="9" cy="14" r="1"/><circle cx="15" cy="14" r="1"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div> <script>
|
</div> <script>
|
||||||
// === State ===
|
|
||||||
const el = {
|
const el = {
|
||||||
chat: document.getElementById('chat'),
|
chat: document.getElementById('chat'),
|
||||||
messages: document.getElementById('messages'),
|
messages: document.getElementById('messages'),
|
||||||
@@ -104,7 +78,6 @@
|
|||||||
modelName: document.getElementById('modelName'),
|
modelName: document.getElementById('modelName'),
|
||||||
modelBadge: document.getElementById('modelBadge'),
|
modelBadge: document.getElementById('modelBadge'),
|
||||||
setApiBtn: document.getElementById('setApiBtn'),
|
setApiBtn: document.getElementById('setApiBtn'),
|
||||||
setModelBtn: document.getElementById('setModelBtn'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = {
|
const store = {
|
||||||
@@ -114,35 +87,22 @@
|
|||||||
set model(v) { localStorage.setItem('openrouter_model', v || 'openai/gpt-4o'); el.modelName.textContent = store.model; },
|
set model(v) { localStorage.setItem('openrouter_model', v || 'openai/gpt-4o'); el.modelName.textContent = store.model; },
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = { messages: [], busy: false, controller: null };
|
||||||
messages: [], // {role: 'user'|'assistant'|'system', content: string}
|
|
||||||
busy: false,
|
|
||||||
controller: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// === UI Helpers ===
|
|
||||||
function addMessage(role, content) {
|
function addMessage(role, content) {
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'flex gap-3';
|
row.className = 'flex gap-3';
|
||||||
|
|
||||||
const avatar = document.createElement('div');
|
const avatar = document.createElement('div');
|
||||||
avatar.className = 'shrink-0 h-8 w-8 rounded-full flex items-center justify-center ' + (role === 'user' ? 'bg-blue-100 text-blue-700' : 'bg-emerald-100 text-emerald-700');
|
avatar.className = 'shrink-0 h-8 w-8 rounded-full flex items-center justify-center ' + (role === 'user' ? 'bg-blue-100 text-blue-700' : 'bg-emerald-100 text-emerald-700');
|
||||||
avatar.textContent = role === 'user' ? '🧑' : '🤖';
|
avatar.textContent = role === 'user' ? '🧑' : '🤖';
|
||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = 'rounded-2xl px-4 py-3 text-[15px] leading-relaxed whitespace-pre-wrap ' + (role === 'user' ? 'bg-blue-50 border border-blue-100' : 'bg-gray-100');
|
bubble.className = 'rounded-2xl px-4 py-3 text-[15px] leading-relaxed whitespace-pre-wrap ' + (role === 'user' ? 'bg-blue-50 border border-blue-100' : 'bg-gray-100');
|
||||||
bubble.textContent = content;
|
bubble.textContent = content;
|
||||||
|
|
||||||
row.appendChild(avatar);
|
row.appendChild(avatar);
|
||||||
row.appendChild(bubble);
|
row.appendChild(bubble);
|
||||||
el.messages.appendChild(row);
|
el.messages.appendChild(row);
|
||||||
|
|
||||||
// Keep internal transcript (for API calls)
|
|
||||||
state.messages.push({ role, content });
|
state.messages.push({ role, content });
|
||||||
|
|
||||||
// Auto-scroll
|
|
||||||
queueMicrotask(() => el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: 'smooth' }));
|
queueMicrotask(() => el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: 'smooth' }));
|
||||||
|
|
||||||
return bubble;
|
return bubble;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,12 +128,7 @@
|
|||||||
if (intro) {
|
if (intro) {
|
||||||
const introRow = document.createElement('div');
|
const introRow = document.createElement('div');
|
||||||
introRow.className = 'flex gap-3';
|
introRow.className = 'flex gap-3';
|
||||||
introRow.innerHTML = `
|
introRow.innerHTML = `<div class="shrink-0 h-8 w-8 rounded-full bg-emerald-100 text-emerald-700 flex items-center justify-center">🤖</div><div class="bg-gray-100 rounded-2xl px-4 py-3 text-[15px] leading-relaxed"><div class="font-semibold text-gray-800 mb-1">New chat</div>You're in a fresh conversation. Ask away.</div>`;
|
||||||
<div class="shrink-0 h-8 w-8 rounded-full bg-emerald-100 text-emerald-700 flex items-center justify-center">🤖</div>
|
|
||||||
<div class="bg-gray-100 rounded-2xl px-4 py-3 text-[15px] leading-relaxed">
|
|
||||||
<div class="font-semibold text-gray-800 mb-1">New chat</div>
|
|
||||||
You're in a fresh conversation. Ask away.
|
|
||||||
</div>`;
|
|
||||||
el.messages.appendChild(introRow);
|
el.messages.appendChild(introRow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,37 +139,21 @@
|
|||||||
el.status.className = 'text-[11px] ' + (online ? 'text-emerald-600' : 'text-gray-400');
|
el.status.className = 'text-[11px] ' + (online ? 'text-emerald-600' : 'text-gray-400');
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Comms ===
|
|
||||||
async function askOpenRouter() {
|
async function askOpenRouter() {
|
||||||
const apiKey = store.apiKey;
|
const apiKey = store.apiKey;
|
||||||
const model = store.model;
|
const model = store.model;
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
// Local demo reply
|
return { ok: true, text: localDemoReply(state.messages[state.messages.length - 1]?.content || '') };
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
text: localDemoReply(state.messages[state.messages.length - 1]?.content || '')
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
state.controller = new AbortController();
|
state.controller = new AbortController();
|
||||||
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify({ model, messages: state.messages.filter(m => m.role !== 'system'), stream: false }),
|
||||||
'Authorization': 'Bearer ' + apiKey,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model,
|
|
||||||
messages: state.messages.filter(m => m.role !== 'system'),
|
|
||||||
stream: false,
|
|
||||||
}),
|
|
||||||
signal: state.controller.signal,
|
signal: state.controller.signal,
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) throw new Error(await res.text() || ('HTTP ' + res.status));
|
||||||
const err = await res.text();
|
|
||||||
throw new Error(err || ('HTTP ' + res.status));
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const choice = data.choices?.[0]?.message?.content?.trim() || '(no content)';
|
const choice = data.choices?.[0]?.message?.content?.trim() || '(no content)';
|
||||||
return { ok: true, text: choice };
|
return { ok: true, text: choice };
|
||||||
@@ -227,18 +166,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function localDemoReply(prompt) {
|
function localDemoReply(prompt) {
|
||||||
// Very tiny offline echo with a dash of personality
|
const tips = [ 'Tip: set an OpenRouter API key (bottom-left) to go truly online.', 'Pro move: click the model badge to change model.', 'New chats are stateless here—no history is kept.' ];
|
||||||
const tips = [
|
|
||||||
'Tip: set an OpenRouter API key (bottom-left) to go truly online.',
|
|
||||||
'Pro move: tap the bot icon to pick a different model.',
|
|
||||||
'New chats are stateless here—no history is kept.',
|
|
||||||
];
|
|
||||||
const tip = tips[Math.floor(Math.random() * tips.length)];
|
const tip = tips[Math.floor(Math.random() * tips.length)];
|
||||||
const mirrored = prompt.split(/\s+/).slice(0, 24).join(' ');
|
const mirrored = prompt.split(/\s+/).slice(0, 24).join(' ');
|
||||||
return `Local demo mode. You said: "${mirrored}"\n\n${tip}`;
|
return `Local demo mode. You said: "${mirrored}"\n\n${tip}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Events ===
|
|
||||||
el.composer.addEventListener('submit', async (e) => {
|
el.composer.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (state.busy) return;
|
if (state.busy) return;
|
||||||
@@ -246,32 +179,25 @@
|
|||||||
if (!text) return;
|
if (!text) return;
|
||||||
el.input.value = '';
|
el.input.value = '';
|
||||||
el.input.style.height = 'auto';
|
el.input.style.height = 'auto';
|
||||||
|
|
||||||
addMessage('user', text);
|
addMessage('user', text);
|
||||||
state.busy = true;
|
state.busy = true;
|
||||||
el.sendBtn.disabled = true;
|
el.sendBtn.disabled = true;
|
||||||
const thinking = addThinkingBubble();
|
const thinking = addThinkingBubble();
|
||||||
|
|
||||||
const res = await askOpenRouter();
|
const res = await askOpenRouter();
|
||||||
thinking.textContent = res.text;
|
thinking.textContent = res.text;
|
||||||
|
|
||||||
// Replace thinking bubble with final assistant bubble style
|
|
||||||
thinking.className = 'rounded-2xl px-4 py-3 text-[15px] leading-relaxed bg-gray-100 text-gray-800 whitespace-pre-wrap';
|
thinking.className = 'rounded-2xl px-4 py-3 text-[15px] leading-relaxed bg-gray-100 text-gray-800 whitespace-pre-wrap';
|
||||||
state.messages[state.messages.length - 1] = { role: 'assistant', content: res.text };
|
state.messages[state.messages.length - 1] = { role: 'assistant', content: res.text };
|
||||||
|
|
||||||
el.sendBtn.disabled = false;
|
el.sendBtn.disabled = false;
|
||||||
state.busy = false;
|
state.busy = false;
|
||||||
queueMicrotask(() => el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: 'smooth' }));
|
queueMicrotask(() => el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: 'smooth' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-resize textarea
|
|
||||||
el.input.addEventListener('input', () => {
|
el.input.addEventListener('input', () => {
|
||||||
el.input.style.height = 'auto';
|
el.input.style.height = 'auto';
|
||||||
const h = Math.min(el.input.scrollHeight, 160); // cap at 160px
|
const h = Math.min(el.input.scrollHeight, 160);
|
||||||
el.input.style.height = h + 'px';
|
el.input.style.height = h + 'px';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enter to send, Shift+Enter for new line
|
|
||||||
el.input.addEventListener('keydown', (e) => {
|
el.input.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -279,30 +205,21 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear chat
|
|
||||||
el.clearBtn.addEventListener('click', () => clearChat(false));
|
el.clearBtn.addEventListener('click', () => clearChat(false));
|
||||||
|
|
||||||
// New chat (fresh intro)
|
|
||||||
el.newChatBtn.addEventListener('click', () => clearChat(true));
|
el.newChatBtn.addEventListener('click', () => clearChat(true));
|
||||||
|
|
||||||
// Set API key
|
|
||||||
el.setApiBtn.addEventListener('click', () => {
|
el.setApiBtn.addEventListener('click', () => {
|
||||||
const current = store.apiKey ? '********' : '';
|
const current = store.apiKey ? '********' : '';
|
||||||
const input = prompt('Enter OpenRouter API key (stored locally):', current);
|
const input = prompt('Enter OpenRouter API key (stored locally):', current);
|
||||||
if (input === null) return; // cancel
|
if (input === null) return;
|
||||||
store.apiKey = input === '********' ? store.apiKey : (input.trim());
|
store.apiKey = input === '********' ? store.apiKey : (input.trim());
|
||||||
alert(store.apiKey ? 'API key saved locally.' : 'API key cleared.');
|
alert(store.apiKey ? 'API key saved locally.' : 'API key cleared.');
|
||||||
});
|
});
|
||||||
|
el.modelBadge.addEventListener('click', () => {
|
||||||
// Set Model
|
|
||||||
el.setModelBtn.addEventListener('click', () => {
|
|
||||||
const input = prompt('Enter model name (e.g., openai/gpt-4o):', store.model);
|
const input = prompt('Enter model name (e.g., openai/gpt-4o):', store.model);
|
||||||
if (input === null) return;
|
if (input === null) return;
|
||||||
const name = input.trim() || 'openai/gpt-4o';
|
store.model = input.trim() || 'openai/gpt-4o';
|
||||||
store.model = name;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Init
|
|
||||||
updateStatus();
|
updateStatus();
|
||||||
el.modelName.textContent = store.model;
|
el.modelName.textContent = store.model;
|
||||||
</script></body>
|
</script></body>
|
||||||
|
|||||||
Reference in New Issue
Block a user