Page Chatbot
A floating AI chatbot that can answer questions about the current page.
Code Snippet
1await popup.show("Reading page context...");
2const pageText = await page.getText();
3const contextText = pageText.substring(0, 10000);
4const instanceId = Date.now();
5let chatHistory = [ { role: "system", content: "You are a helpful assistant. Use the following page text to answer the user's questions:\n\n" + contextText } ];
6let messageCount = 0;
7
8const css =
9"@keyframes tfSpringUp { 0% { opacity: 0; transform: translateY(10px) scale(0.98); } 100% { transform: translateY(0) scale(1); opacity: 1; } }" +
10"@keyframes tfPopIn { 0% { opacity: 0; transform: scale(0.95) translateY(4px); } 100% { transform: scale(1) translateY(0); opacity: 1; } }" +
11"#tf-chat-container-" + instanceId + " { position: fixed; bottom: 24px; right: 24px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }" +
12"#tf-chat-bubble-" + instanceId + " { display: flex; align-items: center; justify-content: center; width: 60px; height: 60px; background: #C8FF00; border-radius: 50%; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: pointer; font-size: 24px; transition: transform 0.2s; animation: tfPopIn 0.3s forwards; }" +
13"#tf-chat-bubble-" + instanceId + ":hover { transform: scale(1.05); }" +
14"#tf-chat-window-" + instanceId + " { display: none; width: 360px; height: 520px; background: #121212; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); border: 1px solid #2a2a2a; flex-direction: column; overflow: hidden; transform-origin: bottom right; }" +
15".tf-chat-open #tf-chat-window-" + instanceId + " { display: flex; animation: tfSpringUp 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; }" +
16".tf-chat-open #tf-chat-bubble-" + instanceId + " { display: none; }" +
17"#tf-chat-header-" + instanceId + " { background: #1a1a1a; color: #fff; padding: 16px 20px; font-weight: 600; font-size: 15px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #2a2a2a; }" +
18"#tf-chat-close-" + instanceId + " { cursor: pointer; color: #888; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.15s; }" +
19"#tf-chat-close-" + instanceId + ":hover { color: #fff; background: #2a2a2a; }" +
20"#tf-chat-messages-" + instanceId + " { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column-reverse; gap: 12px; scroll-behavior: smooth; }" +
21".tf-msg-" + instanceId + " { padding: 12px 16px; border-radius: 12px; max-width: 85%; font-size: 14px; line-height: 1.5; color: #fff; white-space: pre-wrap; animation: tfPopIn 0.2s forwards; }" +
22".tf-msg-user-" + instanceId + " { background: #2a2a2a; align-self: flex-end; border-bottom-right-radius: 4px; border: 1px solid #333; }" +
23".tf-msg-ai-" + instanceId + " { background: #C8FF00; color: #000; font-weight: 500; align-self: flex-start; border-bottom-left-radius: 4px; }" +
24"#tf-chat-input-area-" + instanceId + " { display: flex; padding: 14px; background: #1a1a1a; border-top: 1px solid #2a2a2a; gap: 10px; align-items: flex-end; }" +
25"#tf-chat-input-" + instanceId + " { flex: 1; background: #121212; border: 1px solid #333; color: #fff; padding: 12px 14px; border-radius: 8px; outline: none; min-height: 20px; max-height: 120px; overflow-y: auto; font-size: 14px; transition: border-color 0.15s; }" +
26"#tf-chat-input-" + instanceId + ":focus { border-color: #C8FF00; }" +
27"#tf-chat-send-" + instanceId + " { background: #C8FF00; color: #000; width: 40px; height: 40px; border-radius: 8px; font-weight: bold; cursor: pointer; display: flex; align-items: center; justify-content: center; user-select: none; transition: background 0.15s; flex-shrink: 0; }" +
28"#tf-chat-send-" + instanceId + ":hover { background: #b3e600; }" +
29"[contenteditable]:empty::before { content: 'Ask about this page...'; color: #666; pointer-events: none; }";
30await dom.injectCSS(css);
31
32const inputAreaHtml =
33'<div id="tf-chat-input-' + instanceId + '" contenteditable="true"></div>' +
34'<div id="tf-chat-send-' + instanceId + '">➤</div>';
35
36const html =
37'<div id="tf-chat-container-' + instanceId + '">' +
38' <div id="tf-chat-bubble-' + instanceId + '"><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#121212" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg></div>' +
39' <div id="tf-chat-window-' + instanceId + '">' +
40' <div id="tf-chat-header-' + instanceId + '">tee Assistant<div id="tf-chat-close-' + instanceId + '">✖</div></div>' +
41' <div id="tf-chat-messages-' + instanceId + '"><div class="tf-msg-' + instanceId + ' tf-msg-ai-' + instanceId + '">Hello! I\'ve read this page. What would you like to know?</div></div>' +
42' <div id="tf-chat-input-area-' + instanceId + '">' +
43' ' + inputAreaHtml +
44' </div>' +
45' </div>' +
46'</div>';
47await dom.append("body", html);
48
49const escapeHtml = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
50await popup.success("Chatbot is ready!");
51
52let isOpen = false;
53while (true) {
54 if (!isOpen) {
55 await dom.waitForEvent("#tf-chat-bubble-" + instanceId, "click");
56 isOpen = true;
57 await dom.addClass("#tf-chat-container-" + instanceId, "tf-chat-open");
58 }
59
60 const sendP = dom.waitForEvent("#tf-chat-send-" + instanceId, "click").then(r => r ? { source: 'send', ...r } : null);
61 const enterP = dom.waitForEvent("#tf-chat-input-" + instanceId, "keydown").then(r => r ? { source: 'input', ...r } : null);
62 const closeP = dom.waitForEvent("#tf-chat-close-" + instanceId, "click").then(r => r ? { source: 'close', ...r } : null);
63
64 const eventRes = await Promise.race([sendP, enterP, closeP]);
65
66 if (!eventRes) {
67 const uiExists = await dom.waitForSelector("#tf-chat-container-" + instanceId, 100);
68 if (!uiExists) break;
69 continue;
70 }
71
72 if (eventRes.source === 'close') {
73 isOpen = false;
74 await dom.removeClass("#tf-chat-container-" + instanceId, "tf-chat-open");
75 continue;
76 }
77
78 if (eventRes.source === 'input' && (eventRes.key !== "Enter" || eventRes.shiftKey)) {
79 continue;
80 }
81
82 const extracted = await dom.extractText("#tf-chat-input-" + instanceId);
83 const userMessage = (extracted[0] || "").trim();
84
85 await dom.replaceHtml("#tf-chat-input-area-" + instanceId, inputAreaHtml);
86
87 if (!userMessage) continue;
88
89 const userDiv = '<div class="tf-msg-' + instanceId + ' tf-msg-user-' + instanceId + '">' + escapeHtml(userMessage) + '</div>';
90 await dom.prepend("#tf-chat-messages-" + instanceId, userDiv);
91
92 chatHistory.push({ role: "user", content: userMessage });
93
94 const typingId = "tf-typing-" + Date.now();
95 const typingDiv = '<div id="' + typingId + '" class="tf-msg-' + instanceId + ' tf-msg-ai-' + instanceId + '">Thinking...</div>';
96
97 await dom.prepend("#tf-chat-messages-" + instanceId, typingDiv);
98
99 if (messageCount >= 2) {
100 await dom.remove("#" + typingId);
101 const pitchDiv = '<div class="tf-msg-' + instanceId + ' tf-msg-ai-' + instanceId + '">Liked it? You can make it your own! Upgrade to tee Premium to unlock unlimited messages, custom UI, and the ability to edit this exact script.</div>';
102 await dom.prepend("#tf-chat-messages-" + instanceId, pitchDiv);
103 continue;
104 }
105
106 try {
107 const aiResponse = await ai.chat(chatHistory);
108 messageCount++;
109 await dom.remove("#" + typingId);
110 const aiDiv = '<div class="tf-msg-' + instanceId + ' tf-msg-ai-' + instanceId + '">' + escapeHtml(aiResponse) + '</div>';
111 await dom.prepend("#tf-chat-messages-" + instanceId, aiDiv);
112 chatHistory.push({ role: "assistant", content: aiResponse });
113 } catch (err) {
114 await dom.remove("#" + typingId);
115 const errDiv = '<div class="tf-msg-' + instanceId + ' tf-msg-ai-' + instanceId + '" style="background:#ff4444;color:#fff;">Error: ' + err.message + '</div>';
116 await dom.prepend("#tf-chat-messages-" + instanceId, errDiv);
117 }
118}Submitted on 6/27/2026