Skip to content

Commit 698bfd7

Browse files
authored
Add files via upload
1 parent 0adf33a commit 698bfd7

1 file changed

Lines changed: 385 additions & 0 deletions

File tree

assistant.html

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
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" />
6+
<title>dev_assistant</title>
7+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Syne:wght@400;600;800&display=swap" rel="stylesheet" />
8+
<style>
9+
*{box-sizing:border-box;margin:0;padding:0}
10+
body{background:#0a0c10;font-family:'JetBrains Mono',monospace;height:100vh;overflow:hidden;color:#e2e8f0}
11+
body::before{content:'';position:fixed;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,255,136,0.01) 2px,rgba(0,255,136,0.01) 4px);pointer-events:none;z-index:0}
12+
13+
#assistant{position:fixed;width:80px;height:80px;cursor:pointer;z-index:100;bottom:100px;right:80px;user-select:none;filter:drop-shadow(0 0 8px rgba(0,255,136,0.4));transition:filter .3s}
14+
#assistant:hover{filter:drop-shadow(0 0 20px rgba(0,255,136,0.9))}
15+
#assistant.idle{animation:idleFloat 3s ease-in-out infinite}
16+
#assistant.speaking{animation:speakBounce .35s ease infinite alternate}
17+
#assistant.thinking{animation:thinkSpin .8s linear infinite}
18+
19+
@keyframes idleFloat{0%,100%{transform:translateY(0) rotate(0deg)}33%{transform:translateY(-8px) rotate(-2deg)}66%{transform:translateY(-4px) rotate(2deg)}}
20+
@keyframes speakBounce{0%{transform:scaleY(1) scaleX(1)}100%{transform:scaleY(1.06) scaleX(.97)}}
21+
@keyframes thinkSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
22+
@keyframes slideUp{from{opacity:0;transform:translateY(12px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}
23+
@keyframes blink{0%,90%,100%{transform:scaleY(1)}95%{transform:scaleY(0.1)}}
24+
@keyframes dotPulse{0%,80%,100%{opacity:.2;transform:scale(.7)}40%{opacity:1;transform:scale(1)}}
25+
26+
#chat-bubble{position:fixed;z-index:101;background:#10141c;border:1px solid #00ff88;width:360px;display:none;flex-direction:column;box-shadow:0 0 40px rgba(0,255,136,0.12),0 8px 32px rgba(0,0,0,.5);animation:slideUp .2s ease}
27+
#chat-bubble.active{display:flex}
28+
29+
.bubble-header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid #1e2535;background:#161b26}
30+
.bubble-name{display:flex;align-items:center;gap:8px}
31+
.bubble-dot{width:6px;height:6px;background:#00ff88;border-radius:50%;animation:dotPulse 2s infinite}
32+
.bubble-title{font-size:11px;color:#00ff88;letter-spacing:1.5px;text-transform:uppercase}
33+
.bubble-close{background:transparent;border:none;color:#64748b;font-size:18px;cursor:pointer;line-height:1;padding:0 2px;transition:color .2s}
34+
.bubble-close:hover{color:#ff4455}
35+
36+
.bubble-messages{overflow-y:auto;padding:12px 14px;max-height:280px;display:flex;flex-direction:column;gap:8px}
37+
.bubble-messages::-webkit-scrollbar{width:2px}
38+
.bubble-messages::-webkit-scrollbar-thumb{background:#1e2535}
39+
40+
.msg{font-size:12px;line-height:1.65;padding:8px 11px;max-width:88%;white-space:pre-wrap;word-break:break-word}
41+
.msg.assistant{background:#161b26;border:1px solid #1e2535;border-left:2px solid #00ff88;color:#e2e8f0;align-self:flex-start}
42+
.msg.user{background:rgba(0,255,136,.07);border:1px solid rgba(0,255,136,.2);color:#e2e8f0;align-self:flex-end;text-align:right}
43+
.msg.error{border-left-color:#ff4455;color:#ff4455}
44+
45+
.typing-wrap{display:flex;gap:4px;align-items:center;padding:10px 11px;background:#161b26;border:1px solid #1e2535;border-left:2px solid #00ff88;align-self:flex-start;width:52px}
46+
.td{width:5px;height:5px;background:#00ff88;border-radius:50%;animation:dotPulse 1.2s infinite}
47+
.td:nth-child(2){animation-delay:.2s}
48+
.td:nth-child(3){animation-delay:.4s}
49+
50+
.quick-prompts{display:flex;gap:5px;flex-wrap:wrap;padding:8px 14px;border-top:1px solid #1e2535;background:#0d1017}
51+
.qp{font-size:10px;padding:3px 9px;background:transparent;border:1px solid #1e2535;color:#64748b;font-family:'JetBrains Mono',monospace;cursor:pointer;letter-spacing:.5px;transition:all .2s;white-space:nowrap}
52+
.qp:hover{border-color:#00ff88;color:#00ff88}
53+
54+
.input-row{display:flex;border-top:1px solid #1e2535}
55+
#chat-input{flex:1;background:#0a0c10;border:none;color:#e2e8f0;font-family:'JetBrains Mono',monospace;font-size:12px;padding:10px 12px;outline:none;min-width:0}
56+
#chat-input::placeholder{color:#2a3547}
57+
#send-btn{background:#00ff88;color:#000;border:none;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:700;padding:0 16px;cursor:pointer;letter-spacing:.5px;white-space:nowrap;transition:opacity .2s;flex-shrink:0}
58+
#send-btn:hover{opacity:.85}
59+
#send-btn:disabled{opacity:.35;cursor:not-allowed}
60+
61+
#key-prompt{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:200;background:rgba(10,12,16,.95)}
62+
.kp-box{background:#10141c;border:1px solid #00ff88;padding:40px 48px;width:440px;position:relative}
63+
.kp-corner{position:absolute;width:12px;height:12px;border-color:#00ff88;border-style:solid}
64+
.kp-corner.tl{top:-1px;left:-1px;border-width:2px 0 0 2px}
65+
.kp-corner.tr{top:-1px;right:-1px;border-width:2px 2px 0 0}
66+
.kp-corner.bl{bottom:-1px;left:-1px;border-width:0 0 2px 2px}
67+
.kp-corner.br{bottom:-1px;right:-1px;border-width:0 2px 2px 0}
68+
.kp-title{font-family:'Syne',sans-serif;font-size:26px;font-weight:800;color:#00ff88;margin-bottom:4px}
69+
.kp-sub{font-size:11px;color:#64748b;margin-bottom:28px;line-height:1.7}
70+
.kp-label{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;margin-bottom:6px;margin-top:14px;display:block}
71+
.kp-select,.kp-input{width:100%;background:#161b26;border:1px solid #1e2535;color:#e2e8f0;font-family:'JetBrains Mono',monospace;font-size:12px;padding:9px 13px;outline:none;transition:border-color .2s;margin-bottom:0}
72+
.kp-select{cursor:pointer;color:#64748b}
73+
.kp-input:focus,.kp-select:focus{border-color:#00ff88}
74+
.kp-btn{width:100%;background:#00ff88;color:#000;border:none;font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:700;letter-spacing:1px;padding:12px;cursor:pointer;text-transform:uppercase;transition:opacity .2s;margin-top:20px}
75+
.kp-btn:hover{opacity:.85}
76+
77+
#hint{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);font-size:10px;color:#1e2535;letter-spacing:1px;text-transform:uppercase;pointer-events:none}
78+
</style>
79+
</head>
80+
<body>
81+
82+
<div id="key-prompt">
83+
<div class="kp-box">
84+
<div class="kp-corner tl"></div><div class="kp-corner tr"></div>
85+
<div class="kp-corner bl"></div><div class="kp-corner br"></div>
86+
<div class="kp-title">dev_assistant</div>
87+
<div class="kp-sub">Your roaming AI companion. Bring your own key.</div>
88+
<label class="kp-label">provider</label>
89+
<select class="kp-select" id="kp-provider">
90+
<option value="openrouter">OpenRouter</option>
91+
<option value="gemini">Gemini</option>
92+
</select>
93+
<label class="kp-label">api key</label>
94+
<input class="kp-input" id="kp-key" type="password" placeholder="sk-or-v1-... or AIzaSy..." autocomplete="off" spellcheck="false" />
95+
<button class="kp-btn" onclick="startAssistant()">[ launch ]</button>
96+
</div>
97+
</div>
98+
99+
<div id="assistant" class="idle" onclick="toggleChat()" style="display:none">
100+
<svg viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
101+
<defs>
102+
<filter id="glow"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
103+
</defs>
104+
<!-- Body -->
105+
<rect x="18" y="30" width="44" height="32" rx="7" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/>
106+
<!-- Head -->
107+
<rect x="22" y="8" width="36" height="26" rx="6" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/>
108+
<!-- Antenna -->
109+
<line x1="40" y1="8" x2="40" y2="1" stroke="#00ff88" stroke-width="1.5"/>
110+
<circle cx="40" cy="1" r="2.5" fill="#00ff88" filter="url(#glow)"/>
111+
<!-- Eyes -->
112+
<ellipse id="eye-l" cx="32" cy="20" rx="4" ry="4" fill="#00ff88" filter="url(#glow)"/>
113+
<ellipse id="eye-r" cx="48" cy="20" rx="4" ry="4" fill="#00ff88" filter="url(#glow)"/>
114+
<circle cx="33" cy="20" r="1.8" fill="#0a0c10"/>
115+
<circle cx="49" cy="20" r="1.8" fill="#0a0c10"/>
116+
<circle cx="33.8" cy="19.2" r=".7" fill="#fff" opacity=".8"/>
117+
<circle cx="49.8" cy="19.2" r=".7" fill="#fff" opacity=".8"/>
118+
<!-- Neck -->
119+
<rect x="35" y="33" width="10" height="5" rx="2" fill="#1e2535"/>
120+
<!-- Mouth -->
121+
<rect id="mouth" x="31" y="27" width="18" height="4" rx="2" fill="#00ff88" opacity=".5"/>
122+
<!-- Arms -->
123+
<rect x="5" y="35" width="13" height="6" rx="3" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/>
124+
<rect x="62" y="35" width="13" height="6" rx="3" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/>
125+
<!-- Legs -->
126+
<rect x="25" y="60" width="10" height="14" rx="4" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/>
127+
<rect x="45" y="60" width="10" height="14" rx="4" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/>
128+
<!-- Chest panel -->
129+
<rect x="26" y="38" width="28" height="18" rx="3" fill="#161b26" stroke="#1e2535" stroke-width="1"/>
130+
<circle cx="33" cy="47" r="2.5" fill="#00ff88" opacity=".6"/>
131+
<circle cx="40" cy="47" r="2.5" fill="#4a9eff" opacity=".6"/>
132+
<circle cx="47" cy="47" r="2.5" fill="#f5a623" opacity=".6"/>
133+
<rect x="30" y="41" width="20" height="2" rx="1" fill="#1e2535"/>
134+
</svg>
135+
</div>
136+
137+
<div id="chat-bubble">
138+
<div class="bubble-header">
139+
<div class="bubble-name">
140+
<div class="bubble-dot"></div>
141+
<span class="bubble-title">dev_assistant</span>
142+
</div>
143+
<button class="bubble-close" onclick="toggleChat()">×</button>
144+
</div>
145+
<div class="bubble-messages" id="messages"></div>
146+
<div class="quick-prompts">
147+
<button class="qp" onclick="quickPrompt('What should I focus on today?')">today?</button>
148+
<button class="qp" onclick="quickPrompt('Review my stack')">stack</button>
149+
<button class="qp" onclick="quickPrompt('Give me a quick coding challenge in Python')">challenge</button>
150+
<button class="qp" onclick="quickPrompt('Explain CORS simply')">cors?</button>
151+
<button class="qp" onclick="quickPrompt('Best practices for REST APIs')">rest api</button>
152+
</div>
153+
<div class="input-row">
154+
<input id="chat-input" placeholder="ask anything..." autocomplete="off" spellcheck="false" />
155+
<button id="send-btn" onclick="sendMessage()">send →</button>
156+
</div>
157+
</div>
158+
159+
<div id="hint">click the robot to chat</div>
160+
161+
<script>
162+
let API_KEY = '', PROVIDER = 'openrouter';
163+
let chatOpen = false;
164+
let messages = [];
165+
let posX, posY;
166+
let roamTimer = null;
167+
168+
const bot = document.getElementById('assistant');
169+
const bubble = document.getElementById('chat-bubble');
170+
const msgsEl = document.getElementById('messages');
171+
const inp = document.getElementById('chat-input');
172+
const sendBtn = document.getElementById('send-btn');
173+
const hint = document.getElementById('hint');
174+
175+
// ── Init ──────────────────────────────────────────────────────
176+
window.addEventListener('DOMContentLoaded', () => {
177+
const k = localStorage.getItem('da_key');
178+
const p = localStorage.getItem('da_provider');
179+
if (k) { API_KEY = k; PROVIDER = p || 'openrouter'; launch(); }
180+
inp.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); });
181+
document.getElementById('kp-key').addEventListener('keydown', e => { if (e.key === 'Enter') startAssistant(); });
182+
});
183+
184+
function startAssistant() {
185+
const k = document.getElementById('kp-key').value.trim();
186+
const p = document.getElementById('kp-provider').value;
187+
if (!k) { document.getElementById('kp-key').style.borderColor = '#ff4455'; return; }
188+
API_KEY = k; PROVIDER = p;
189+
localStorage.setItem('da_key', k);
190+
localStorage.setItem('da_provider', p);
191+
document.getElementById('key-prompt').style.display = 'none';
192+
launch();
193+
}
194+
195+
function launch() {
196+
document.getElementById('key-prompt').style.display = 'none';
197+
bot.style.display = 'block';
198+
posX = window.innerWidth - 140;
199+
posY = window.innerHeight - 180;
200+
bot.style.left = posX + 'px';
201+
bot.style.top = posY + 'px';
202+
bot.style.bottom = 'auto';
203+
bot.style.right = 'auto';
204+
startRoaming();
205+
setTimeout(greet, 600);
206+
}
207+
208+
// ── Roaming ───────────────────────────────────────────────────
209+
function startRoaming() {
210+
roamTimer = setInterval(() => { if (!chatOpen) roam(); }, 5000);
211+
}
212+
213+
function roam() {
214+
const pw = window.innerWidth - 100, ph = window.innerHeight - 100;
215+
const dx = (Math.random() - 0.5) * 200, dy = (Math.random() - 0.5) * 140;
216+
posX = Math.max(10, Math.min(pw, posX + dx));
217+
posY = Math.max(10, Math.min(ph, posY + dy));
218+
bot.style.transition = 'left 2.5s cubic-bezier(.25,.46,.45,.94), top 2.5s cubic-bezier(.25,.46,.45,.94)';
219+
bot.style.left = posX + 'px';
220+
bot.style.top = posY + 'px';
221+
syncBubble();
222+
}
223+
224+
function syncBubble() {
225+
const bw = 360, bh = 400;
226+
const vw = window.innerWidth, vh = window.innerHeight;
227+
let bx = posX + 90, by = posY - 60;
228+
if (bx + bw > vw - 8) bx = posX - bw - 8;
229+
if (by + bh > vh - 8) by = posY - bh + 80;
230+
if (by < 8) by = posY + 90;
231+
bubble.style.left = bx + 'px';
232+
bubble.style.top = by + 'px';
233+
bubble.style.bottom = 'auto';
234+
bubble.style.right = 'auto';
235+
}
236+
237+
// ── Toggle chat ───────────────────────────────────────────────
238+
function toggleChat() {
239+
chatOpen = !chatOpen;
240+
if (chatOpen) {
241+
syncBubble();
242+
bubble.classList.add('active');
243+
setAnim('speaking');
244+
hint.style.display = 'none';
245+
inp.focus();
246+
} else {
247+
bubble.classList.remove('active');
248+
setAnim('idle');
249+
hint.style.display = 'block';
250+
}
251+
}
252+
253+
function setAnim(state) {
254+
bot.className = state;
255+
}
256+
257+
// ── Messages ──────────────────────────────────────────────────
258+
function addMsg(role, text, extra = '') {
259+
messages.push({ role, content: text });
260+
const el = document.createElement('div');
261+
el.className = `msg ${role}${extra ? ' ' + extra : ''}`;
262+
el.textContent = text;
263+
msgsEl.appendChild(el);
264+
msgsEl.scrollTop = msgsEl.scrollHeight;
265+
return el;
266+
}
267+
268+
function showTyping() {
269+
const el = document.createElement('div');
270+
el.className = 'typing-wrap'; el.id = 'typing';
271+
el.innerHTML = '<div class="td"></div><div class="td"></div><div class="td"></div>';
272+
msgsEl.appendChild(el);
273+
msgsEl.scrollTop = msgsEl.scrollHeight;
274+
}
275+
276+
function hideTyping() {
277+
const el = document.getElementById('typing');
278+
if (el) el.remove();
279+
}
280+
281+
function greet() {
282+
const g = [
283+
"Hey dev! I'm your assistant. Ask me anything — code, debugging, architecture, or just vibe.",
284+
"// online\n\nReady to help. What are we building today?",
285+
"Hello! I'm here whenever you need me. Ask away.",
286+
];
287+
addMsg('assistant', g[Math.floor(Math.random() * g.length)]);
288+
}
289+
290+
function quickPrompt(t) { inp.value = t; sendMessage(); }
291+
292+
// ── Send ──────────────────────────────────────────────────────
293+
async function sendMessage() {
294+
const text = inp.value.trim();
295+
if (!text || sendBtn.disabled) return;
296+
inp.value = '';
297+
addMsg('user', text);
298+
sendBtn.disabled = true;
299+
setAnim('thinking');
300+
showTyping();
301+
302+
const sys = `You are dev_assistant — a concise, sharp AI coding assistant with a hacker aesthetic.
303+
Expertise: cybersecurity, IoT, Python, C/C++, Go, JavaScript/TypeScript, Linux, EdTech, DevOps.
304+
Be direct and practical. Use code blocks when relevant. Skip filler phrases.
305+
Keep replies under 200 words unless a detailed answer is genuinely needed.`;
306+
307+
const history = messages.slice(-12).map(m => ({
308+
role: m.role === 'assistant' ? 'assistant' : 'user',
309+
content: m.content
310+
}));
311+
312+
try {
313+
let reply = '';
314+
315+
if (PROVIDER === 'openrouter') {
316+
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
317+
method: 'POST',
318+
headers: {
319+
'Authorization': `Bearer ${API_KEY}`,
320+
'Content-Type': 'application/json',
321+
'HTTP-Referer': window.location.href,
322+
'X-Title': 'dev_assistant',
323+
},
324+
body: JSON.stringify({
325+
model: 'meta-llama/llama-3.3-70b-instruct',
326+
messages: [{ role: 'system', content: sys }, ...history],
327+
max_tokens: 600,
328+
temperature: 0.72,
329+
}),
330+
});
331+
const d = await res.json();
332+
if (d.error) throw new Error(d.error.message);
333+
reply = d.choices?.[0]?.message?.content || '...';
334+
335+
} else {
336+
const contents = history.map(m => ({
337+
role: m.role === 'assistant' ? 'model' : 'user',
338+
parts: [{ text: m.content }]
339+
}));
340+
const res = await fetch(
341+
`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${API_KEY}`,
342+
{
343+
method: 'POST',
344+
headers: { 'Content-Type': 'application/json' },
345+
body: JSON.stringify({
346+
system_instruction: { parts: [{ text: sys }] },
347+
contents,
348+
generationConfig: { maxOutputTokens: 600, temperature: 0.72 },
349+
}),
350+
}
351+
);
352+
const d = await res.json();
353+
if (d.error) throw new Error(d.error.message);
354+
reply = d.candidates?.[0]?.content?.parts?.[0]?.text || '...';
355+
}
356+
357+
hideTyping();
358+
addMsg('assistant', reply);
359+
setAnim('speaking');
360+
setTimeout(() => { if (chatOpen) setAnim('idle'); }, 1800);
361+
362+
} catch (e) {
363+
hideTyping();
364+
addMsg('assistant', `// error: ${e.message}`, 'error');
365+
setAnim('idle');
366+
}
367+
368+
sendBtn.disabled = false;
369+
inp.focus();
370+
}
371+
372+
// ── Eye blink ─────────────────────────────────────────────────
373+
setInterval(() => {
374+
if (Math.random() > 0.65) {
375+
['eye-l','eye-r'].forEach(id => {
376+
const el = document.getElementById(id);
377+
if (!el) return;
378+
el.setAttribute('ry', '0.4');
379+
setTimeout(() => el.setAttribute('ry', '4'), 110);
380+
});
381+
}
382+
}, 2800);
383+
</script>
384+
</body>
385+
</html>

0 commit comments

Comments
 (0)