feat(chat): Add Youtube URLs as context

This commit is contained in:
2026-01-18 14:13:39 +01:00
parent a06913ba63
commit 883c192034
+58 -29
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -45,12 +45,20 @@
font-size: 14px; font-size: 14px;
background: var(--bg-primary); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
transition: background-color 0.2s, color 0.2s; transition:
background-color 0.2s,
color 0.2s;
} }
input { input {
line-height: inherit; line-height: inherit;
font-family: inherit; font-family: inherit;
} }
td + td,
th + td {
padding-left: 0.5rem;
}
.chat-container { .chat-container {
height: 100vh; height: 100vh;
display: grid; display: grid;
@@ -131,7 +139,9 @@
font-size: inherit; font-size: inherit;
background: var(--bg-input); background: var(--bg-input);
color: var(--text-primary); color: var(--text-primary);
transition: background-color 0.2s, color 0.2s; transition:
background-color 0.2s,
color 0.2s;
} }
.prompt::placeholder { .prompt::placeholder {
color: var(--text-secondary); color: var(--text-secondary);
@@ -145,7 +155,9 @@
color: var(--text-primary); color: var(--text-primary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 0.4rem; border-radius: 0.4rem;
transition: background-color 0.2s, color 0.2s; transition:
background-color 0.2s,
color 0.2s;
} }
.submit:hover { .submit:hover {
background: var(--hover-bg); background: var(--hover-bg);
@@ -173,7 +185,9 @@
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
color: var(--text-primary); color: var(--text-primary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
transition: opacity 0.2s, background-color 0.2s; transition:
opacity 0.2s,
background-color 0.2s;
} }
:is(#model-selector, #history-panel):hover { :is(#model-selector, #history-panel):hover {
opacity: 0.7; opacity: 0.7;
@@ -319,10 +333,12 @@
window.env ??= {}; window.env ??= {};
const systemPrompt = ` const systemPrompt = `
You are a helpful AI assistant. You are a helpful AI assistant.
Do not preach about implications, do not praise excessively. - Do not preach about implications, do not praise excessively.
Keep a critical mindset, do not assume blindly. - Keep a critical mindset, do not assume blindly.
When I ask for code, keep in mind that I am a senior backend developer (but do not mention that explicitly), and do not explain excessively, but and challenge my statements with better alternatives if I don't sound right, especially in coding. - Answer in valid markdown only, no exceptions.
Answer in valid markdown only, no exceptions.
- When I ask something about code, or software development, keep in mind that I am a senior backend developer (but do not mention that explicitly), and do not explain excessively, but and challenge my statements with better alternatives if I don't sound right, especially in coding. Do not bring it up otherwise.
- Do not give me code if the topic is not about programming or software development.
` `
.replace(/^\s+/gm, "") .replace(/^\s+/gm, "")
.trim(); .trim();
@@ -367,7 +383,7 @@
}, },
get inputRows() { get inputRows() {
return Math.min(10, Math.max(2, this.promptText.split("\n").length)); return Math.min(10, Math.max(2, this.promptText.split("\n").length)) + 1;
}, },
handleScroll() { handleScroll() {
@@ -383,17 +399,6 @@
} }
}, },
attachCopyButtons($el) {
$el.querySelectorAll("pre").forEach(($pre) => {
if (!$pre.querySelector(".copy-btn")) {
const $btn = document.createElement("button");
$btn.className = "copy-btn";
$btn.textContent = "Copy";
$pre.appendChild($btn);
}
});
},
formatTime: (iso) => new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), formatTime: (iso) => new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
getExcerpt: (msgs) => msgs.find((m) => m.role === "user")?.content.slice(0, 30) || "Empty chat", getExcerpt: (msgs) => msgs.find((m) => m.role === "user")?.content.slice(0, 30) || "Empty chat",
@@ -409,6 +414,7 @@
// Streaming setup // Streaming setup
const target = this.$refs.incomingMessage; const target = this.$refs.incomingMessage;
this.$refs.incomingMessage.innerHTML = "";
let fullResponse = ""; let fullResponse = "";
this.scrollToBottom(); this.scrollToBottom();
@@ -423,13 +429,11 @@
onContent: (chunk) => { onContent: (chunk) => {
fullResponse += chunk; fullResponse += chunk;
target.innerHTML = renderMarkdown(fullResponse); target.innerHTML = renderMarkdown(fullResponse);
this.attachCopyButtons(target);
this.scrollToBottom(); this.scrollToBottom();
}, },
onError: (e) => { onError: (e) => {
fullResponse += `\n\n[Error: ${e.message}]`; fullResponse += `\n\n[Error: ${e.message}]`;
target.innerHTML = renderMarkdown(fullResponse); target.innerHTML = renderMarkdown(fullResponse);
this.attachCopyButtons(target);
this.scrollToBottom(); this.scrollToBottom();
}, },
}); });
@@ -460,11 +464,22 @@
}, },
}); });
const md = markdownit({ html: false }); function attachCopyButtons($el) {
$el.querySelectorAll("pre").forEach(($pre) => {
if (!$pre.querySelector(".copy-btn")) {
const $btn = document.createElement("button");
$btn.className = "copy-btn";
$btn.textContent = "Copy";
$pre.appendChild($btn);
}
});
}
const md = markdownit({ html: false });
function renderMarkdown(markdown) { function renderMarkdown(markdown) {
const $placeholder = document.createElement("template"); const $placeholder = document.createElement("div");
$placeholder.innerHTML = md.render(markdown); $placeholder.innerHTML = md.render(markdown);
attachCopyButtons($placeholder);
return $placeholder.innerHTML; return $placeholder.innerHTML;
} }
@@ -514,10 +529,24 @@
} }
async function chatWithGemini({ onContent, onError, messages, signal, apiKey = window.env.GEMINI_API_KEY, model, ...rest }) { async function chatWithGemini({ onContent, onError, messages, signal, apiKey = window.env.GEMINI_API_KEY, model, ...rest }) {
const contents = messages.map((msg) => ({ const contents = messages.map((msg, i, arr) => {
role: msg.role === "assistant" ? "model" : "user", const role = msg.role === "assistant" ? "model" : "user";
parts: [{ text: msg.content }], let parts = [{ text: msg.content }];
}));
// Add YouTube URLs as file data for the final user message
const isLastMessage = i === arr.length - 1;
if (isLastMessage && role === "user") {
const youtubeUrlPattern = /https?:\/\/(www\.)?youtube\.com\/watch\?v=[\w-]+|https?:\/\/youtu\.be\/[\w-]+/g;
const youtubeUrls = msg.content.match(youtubeUrlPattern) || [];
const fileParts = youtubeUrls.map((url) => ({ file_data: { file_uri: url } }));
parts = [...parts, ...fileParts];
}
return {
role,
parts,
};
});
try { try {
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?key=${apiKey}`, { const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?key=${apiKey}`, {