Skip to content

Commit 6bfdb40

Browse files
committed
Updated the UI
1 parent f145425 commit 6bfdb40

File tree

9 files changed

+1345
-3323
lines changed

9 files changed

+1345
-3323
lines changed

ChatInterface.js

Lines changed: 0 additions & 1423 deletions
This file was deleted.

Screensaver.js

Lines changed: 0 additions & 650 deletions
This file was deleted.

chat.js

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
// chat.js
2+
3+
document.addEventListener("DOMContentLoaded", () => {
4+
const chatBox = document.getElementById("chat-box");
5+
const chatInput = document.getElementById("chat-input");
6+
const sendButton = document.getElementById("send-button");
7+
const clearChatBtn = document.getElementById("clear-chat");
8+
const voiceToggle = document.getElementById("voice-toggle");
9+
const codePanel = document.getElementById("code-panel");
10+
const codeContent = document.getElementById("code-content");
11+
12+
let currentSession = Storage.getCurrentSession();
13+
if (!currentSession) return;
14+
15+
// Helper: Escape HTML for safe rendering.
16+
function escapeHTML(html) {
17+
return html.replace(/&/g, "&")
18+
.replace(/</g, "&lt;")
19+
.replace(/>/g, "&gt;")
20+
.replace(/"/g, "&quot;")
21+
.replace(/'/g, "&#039;");
22+
}
23+
24+
// Helper: Generate a random seed.
25+
function randomSeed() {
26+
return Math.floor(Math.random() * 1000000).toString();
27+
}
28+
29+
// Helper: Extract JSON substring (from first "{" to last "}").
30+
function extractJSON(str) {
31+
const start = str.indexOf("{");
32+
const end = str.lastIndexOf("}");
33+
if (start !== -1 && end !== -1 && end > start) {
34+
return str.substring(start, end + 1);
35+
}
36+
return str;
37+
}
38+
39+
// Process structured response text by extracting sections.
40+
// Returns an array of objects: { type: 'message'|'code'|'image', content: string }
41+
function processResponseSections(text) {
42+
const sections = [];
43+
const regex = /\[(message|code|image)]([\s\S]*?)\[\/\1]/g;
44+
let match;
45+
while ((match = regex.exec(text)) !== null) {
46+
sections.push({
47+
type: match[1],
48+
content: match[2].trim()
49+
});
50+
}
51+
return sections;
52+
}
53+
54+
// Append a structured message (with sections) to the chat UI.
55+
function appendStructuredMessage({ role, content }) {
56+
const container = document.createElement("div");
57+
container.classList.add("structured-message", role === "user" ? "user-message" : "ai-message");
58+
59+
const sections = processResponseSections(content);
60+
if (sections.length === 0) {
61+
// Fallback if no structured sections are found.
62+
container.textContent = content;
63+
} else {
64+
sections.forEach(section => {
65+
if (section.type === "message") {
66+
// Render plain text.
67+
const msgDiv = document.createElement("div");
68+
msgDiv.classList.add("structured-text");
69+
msgDiv.textContent = section.content;
70+
container.appendChild(msgDiv);
71+
} else if (section.type === "code") {
72+
// Create a "Show Code" icon button.
73+
const codeBtn = document.createElement("button");
74+
codeBtn.classList.add("expand-code-btn");
75+
codeBtn.textContent = "Show Code"; // You can replace this text with an icon if desired.
76+
codeBtn.dataset.code = section.content;
77+
codeBtn.addEventListener("click", () => {
78+
// Insert the code as a markdown formatted block for syntax highlighting.
79+
codeContent.innerHTML = `<pre class="language-python"><code>${escapeHTML(section.content)}</code></pre>`;
80+
if (window.Prism) {
81+
const codeEl = codeContent.querySelector("code");
82+
Prism.highlightElement(codeEl);
83+
}
84+
// Simulate clicking the expand icon to switch the view.
85+
document.getElementById("expand-code-btn").click();
86+
});
87+
container.appendChild(codeBtn);
88+
} else if (section.type === "image") {
89+
// Render an image.
90+
const img = document.createElement("img");
91+
img.classList.add("structured-image");
92+
img.src = section.content;
93+
img.alt = "AI Generated Image";
94+
container.appendChild(img);
95+
}
96+
});
97+
}
98+
99+
chatBox.appendChild(container);
100+
chatBox.scrollTop = chatBox.scrollHeight;
101+
}
102+
103+
// Append a message to the chat UI.
104+
function appendMessage({ role, content }) {
105+
if (content.includes("[message]") || content.includes("[code]") || content.includes("[image]")) {
106+
appendStructuredMessage({ role, content });
107+
} else {
108+
const messageDiv = document.createElement("div");
109+
messageDiv.classList.add("message", role === "user" ? "user-message" : "ai-message");
110+
messageDiv.innerHTML = content;
111+
chatBox.appendChild(messageDiv);
112+
chatBox.scrollTop = chatBox.scrollHeight;
113+
}
114+
}
115+
116+
// Add a new message and update session storage.
117+
function addNewMessage({ role, content }) {
118+
appendMessage({ role, content });
119+
currentSession.messages.push({ role, content });
120+
if (currentSession.messages.length > 10) {
121+
currentSession.messages = currentSession.messages.slice(-10);
122+
}
123+
Storage.updateSessionMessages(currentSession.id, currentSession.messages);
124+
}
125+
126+
function renderStoredMessages(messages) {
127+
chatBox.innerHTML = "";
128+
messages.forEach((msg) => {
129+
appendMessage(msg);
130+
});
131+
}
132+
133+
renderStoredMessages(currentSession.messages);
134+
135+
// Voice recognition integration.
136+
let recognition;
137+
if ("webkitSpeechRecognition" in window || "SpeechRecognition" in window) {
138+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
139+
recognition = new SpeechRecognition();
140+
recognition.continuous = false;
141+
recognition.interimResults = false;
142+
143+
recognition.addEventListener("result", (event) => {
144+
const transcript = Array.from(event.results)
145+
.map(result => result[0])
146+
.map(result => result.transcript)
147+
.join("");
148+
chatInput.value += transcript;
149+
sendButton.disabled = chatInput.value.trim() === "";
150+
});
151+
152+
recognition.addEventListener("error", (event) => {
153+
console.error("Speech recognition error:", event.error);
154+
});
155+
}
156+
157+
voiceToggle.addEventListener("click", () => {
158+
if (recognition) {
159+
recognition.start();
160+
} else {
161+
alert("Voice recognition is not supported in this browser.");
162+
}
163+
});
164+
165+
// Send message on Enter key (Shift+Enter for newline).
166+
chatInput.addEventListener("keydown", (e) => {
167+
if (e.key === "Enter" && !e.shiftKey) {
168+
e.preventDefault();
169+
sendButton.click();
170+
}
171+
});
172+
173+
chatInput.addEventListener("input", () => {
174+
sendButton.disabled = chatInput.value.trim() === "";
175+
});
176+
177+
sendButton.addEventListener("click", () => {
178+
const userMessage = chatInput.value.trim();
179+
if (!userMessage) return;
180+
if (currentSession.name === "New Chat" && currentSession.messages.length === 0) {
181+
generateChatTitle(userMessage);
182+
}
183+
addNewMessage({ role: "user", content: userMessage });
184+
chatInput.value = "";
185+
sendButton.disabled = true;
186+
sendToPollinations();
187+
});
188+
189+
clearChatBtn.addEventListener("click", () => {
190+
if (!confirm("Are you sure you want to clear this chat?")) return;
191+
currentSession.messages = [];
192+
Storage.updateSessionMessages(currentSession.id, currentSession.messages);
193+
chatBox.innerHTML = "";
194+
});
195+
196+
// Generate a chat title using a system prompt.
197+
function generateChatTitle(initialInput) {
198+
const prompt = `Generate a creative, 3-8 word chat title based on this conversation:\nUser: ${initialInput}`;
199+
const body = {
200+
messages: [
201+
{
202+
role: "system",
203+
content: "You are a creative assistant. Generate a concise chat title strictly as a raw JSON object with exactly one key 'chatTitle'. Do not output any extra text."
204+
},
205+
{ role: "user", content: prompt }
206+
],
207+
model: "openai",
208+
seed: randomSeed(),
209+
jsonMode: true,
210+
private: true
211+
};
212+
213+
console.debug("Generating chat title with payload:", body);
214+
fetch("https://text.pollinations.ai/openai", {
215+
method: "POST",
216+
headers: { "Content-Type": "application/json" },
217+
body: JSON.stringify(body)
218+
})
219+
.then((res) => res.json())
220+
.then((data) => {
221+
console.debug("Raw title generation response:", data);
222+
if (data?.choices && data.choices.length > 0) {
223+
const titleJSON = data.choices[0].message.content.trim();
224+
try {
225+
const parsedTitle = JSON.parse(titleJSON);
226+
const title = parsedTitle.chatTitle ? parsedTitle.chatTitle : titleJSON;
227+
console.debug("Extracted chat title:", title);
228+
Storage.renameSession(currentSession.id, title);
229+
currentSession = Storage.getCurrentSession();
230+
} catch (e) {
231+
console.error("Error parsing chat title JSON:", e);
232+
}
233+
}
234+
})
235+
.catch((err) => {
236+
console.error("Error generating chat title:", err);
237+
});
238+
}
239+
240+
// Send message to Pollinations API with a system prompt enforcing structured output.
241+
function sendToPollinations() {
242+
const modelName = currentSession.model || "openai";
243+
const messagesToSend = currentSession.messages.slice(-10);
244+
const normalizedMessages = messagesToSend.map((m) => {
245+
return { role: m.role === "ai" ? "assistant" : m.role, content: m.content };
246+
});
247+
const systemPrompt = {
248+
role: "system",
249+
content:
250+
"You are a creative assistant. For every response, output your answer strictly as a raw JSON object with exactly one key 'message'. " +
251+
"The value must be a string divided into clearly delineated sections. Each section must be wrapped with specific tags: " +
252+
"[message] and [/message] for plain text, [code] and [/code] for code sections, and [image] and [/image] for images. " +
253+
"For example, when outputting code, include the actual code in a code block using triple backticks with the language specified, like so: " +
254+
"[code] ```python\nprint(\"Hello World!\")\n``` [/code]. " +
255+
"You may include one or many sections of any type, in any order. Do not output any extra keys or text."
256+
};
257+
const body = {
258+
messages: [systemPrompt, ...normalizedMessages],
259+
model: modelName,
260+
seed: randomSeed(),
261+
jsonMode: true,
262+
private: true
263+
};
264+
265+
console.debug("Sending message payload:", body);
266+
fetch("https://text.pollinations.ai/openai", {
267+
method: "POST",
268+
headers: { "Content-Type": "application/json" },
269+
body: JSON.stringify(body)
270+
})
271+
.then((res) => {
272+
if (!res.ok) {
273+
throw new Error(`HTTP error! Status: ${res.status}`);
274+
}
275+
return res.json();
276+
})
277+
.then((data) => {
278+
console.debug("Raw Pollinations response:", data);
279+
const aiResponse = formatAIResponse(data);
280+
console.debug("Formatted AI response:", aiResponse);
281+
addNewMessage({ role: "ai", content: aiResponse });
282+
})
283+
.catch((err) => {
284+
console.error("Pollinations error:", err);
285+
addNewMessage({ role: "ai", content: "Error reaching Pollinations API: " + err.message });
286+
});
287+
}
288+
289+
// Format the AI response.
290+
// If the parsed JSON contains keys like "message", "code", or "image", wrap them with our custom tags.
291+
function formatAIResponse(data) {
292+
let rawResponse;
293+
if (typeof data === "string") {
294+
rawResponse = extractJSON(data);
295+
console.debug("Extracted JSON from raw response:", rawResponse);
296+
try {
297+
data = JSON.parse(rawResponse);
298+
} catch (e) {
299+
console.error("Failed to parse JSON from response:", e);
300+
return `<pre>${escapeHTML(rawResponse)}</pre>`;
301+
}
302+
}
303+
console.debug("Parsed JSON response:", data);
304+
305+
let content = "";
306+
if (data.choices && data.choices.length > 0 && data.choices[0].message && data.choices[0].message.content) {
307+
content = data.choices[0].message.content.trim();
308+
} else if (data.response) {
309+
content = typeof data.response === "string" ? data.response.trim() : "";
310+
} else {
311+
console.debug("No valid content field in response, returning full JSON.");
312+
return JSON.stringify(data, null, 2);
313+
}
314+
315+
// Try parsing the content as JSON.
316+
let structured;
317+
try {
318+
structured = JSON.parse(content);
319+
} catch (e) {
320+
console.error("Failed to parse response content as JSON:", e);
321+
return `<pre>${escapeHTML(content)}</pre>`;
322+
}
323+
324+
// Build the final structured string with our custom tags.
325+
if (typeof structured === "object" && structured !== null) {
326+
let finalStr = "";
327+
if (structured.message) {
328+
finalStr += "[message]" + structured.message + "[/message]\n";
329+
}
330+
if (structured.code) {
331+
finalStr += "[code] ```python\n" + structured.code + "\n``` [/code]\n";
332+
}
333+
if (structured.image) {
334+
finalStr += "[image]" + structured.image + "[/image]\n";
335+
}
336+
return finalStr.trim() || JSON.stringify(structured, null, 2);
337+
}
338+
return structured;
339+
}
340+
341+
// --- New Code Panel Toggle Controls ---
342+
// These assume that your index.html contains two icon buttons with IDs:
343+
// "minimize-code-btn" (left arrow) and "expand-code-btn" (right arrow)
344+
const expandBtn = document.getElementById("expand-code-btn");
345+
const minimizeBtn = document.getElementById("minimize-code-btn");
346+
const chatLayout = document.querySelector(".chat-layout");
347+
348+
// When the expand button is clicked:
349+
expandBtn.addEventListener("click", () => {
350+
if (chatLayout.classList.contains("code-full")) {
351+
// If already fully expanded, revert to split view.
352+
chatLayout.classList.remove("code-full");
353+
chatLayout.classList.add("split-view");
354+
} else {
355+
// Expand code panel fully: hide chat panel.
356+
chatLayout.classList.remove("chat-only", "split-view");
357+
chatLayout.classList.add("code-full");
358+
}
359+
});
360+
361+
// When the minimize button is clicked:
362+
minimizeBtn.addEventListener("click", () => {
363+
// Minimize the code panel and show only chat responses.
364+
chatLayout.classList.remove("code-full", "split-view");
365+
chatLayout.classList.add("chat-only");
366+
});
367+
});

favicon.ico

37.2 KB
Binary file not shown.

0 commit comments

Comments
 (0)