Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 72 additions & 40 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ const suggestedPrompts = [
"Design a SQL schema for a multi-tenant SaaS app",
];

marked.setOptions({ breaks: true, gfm: true });

const makeMessage = (role, content = "") => ({
id:
typeof crypto !== "undefined" && crypto.randomUUID
Expand Down Expand Up @@ -154,10 +156,25 @@ const sanitizeMarkup = (html) => {
return template.innerHTML;
};

const addCodeCopyButtons = (html) => {
if (typeof document === "undefined") return html;

const template = document.createElement("template");
template.innerHTML = html;
template.content.querySelectorAll("pre").forEach((pre) => {
const button = document.createElement("button");
button.className = "copy-btn";
button.type = "button";
button.textContent = "Copy";
button.setAttribute("aria-label", "Copy code");
pre.appendChild(button);
});
return template.innerHTML;
};

const renderMarkdown = (text) => {
try {
marked.setOptions({ breaks: true, gfm: true });
return sanitizeMarkup(marked.parse(text || ""));
return addCodeCopyButtons(sanitizeMarkup(marked.parse(text || "")));
} catch {
return escapeHtml(text).replace(/\n/g, "<br>");
}
Expand Down Expand Up @@ -259,29 +276,6 @@ function App() {
}
}, [messages]);

useEffect(() => {
const addCopyButtons = () => {
document.querySelectorAll(".message-content pre").forEach((pre) => {
if (pre.querySelector(".copy-btn")) return;
const btn = document.createElement("button");
btn.className = "copy-btn";
btn.type = "button";
btn.textContent = "Copy";
btn.onclick = async () => {
const code =
pre.querySelector("code")?.textContent || pre.textContent || "";
await navigator.clipboard.writeText(code.replace(/Copy$/, "").trim());
btn.textContent = "Copied";
setTimeout(() => {
btn.textContent = "Copy";
}, 1600);
};
pre.appendChild(btn);
});
};
window.requestAnimationFrame(addCopyButtons);
}, [messages]);

const statusLabel = useMemo(() => {
if (status === "ready") return "Ready";
if (status === "busy") return "Working";
Expand Down Expand Up @@ -343,6 +337,26 @@ function App() {
setTimeout(() => setCopiedMessageId(null), 1500);
}, []);

const copyCodeBlock = useCallback(async (event) => {
const target = event.target;
if (!(target instanceof Element)) return;

const button = target.closest(".copy-btn");
if (!button) return;

const pre = button.closest("pre");
const code = pre?.querySelector("code")?.textContent || "";
if (!code) return;

await navigator.clipboard.writeText(code.trim());
button.textContent = "Copied";
button.setAttribute("disabled", "true");
window.setTimeout(() => {
button.textContent = "Copy";
button.removeAttribute("disabled");
}, 1500);
}, []);

const stopGeneration = useCallback(() => {
abortRef.current?.abort();
abortRef.current = null;
Expand Down Expand Up @@ -377,6 +391,24 @@ function App() {
const controller = new AbortController();
abortRef.current = controller;
let fullText = "";
let pendingFrame = 0;
let pendingContent = "";

const flushAssistant = () => {
if (!pendingFrame) return;
window.cancelAnimationFrame(pendingFrame);
pendingFrame = 0;
updateMessage(assistantMessage.id, pendingContent);
};

const scheduleAssistantUpdate = (content) => {
pendingContent = content;
if (pendingFrame) return;
pendingFrame = window.requestAnimationFrame(() => {
pendingFrame = 0;
updateMessage(assistantMessage.id, pendingContent);
});
};

try {
const endpoint = useResearch ? "/generate/research" : "/generate/stream";
Expand Down Expand Up @@ -427,7 +459,7 @@ function App() {
const parsed = JSON.parse(data);
if (parsed.token) {
fullText += parsed.token;
updateMessage(assistantMessage.id, fullText);
scheduleAssistantUpdate(fullText);
} else if (parsed.error) {
throw new Error(parsed.error);
}
Expand All @@ -444,13 +476,15 @@ function App() {
updateMessage(assistantMessage.id, fullText);
}

flushAssistant();
if (!fullText.trim()) {
updateMessage(
assistantMessage.id,
"No response was returned. Try a more specific prompt or raise the token limit.",
);
}
} catch (error) {
flushAssistant();
if (error.name === "AbortError") {
updateMessage(assistantMessage.id, fullText || "Generation stopped.");
} else {
Expand Down Expand Up @@ -526,6 +560,7 @@ function App() {
copiedMessageId={copiedMessageId}
messages={messages}
messagesRef={messagesRef}
onCodeCopy={copyCodeBlock}
onCopy={copyMessage}
/>
)}
Expand Down Expand Up @@ -729,19 +764,10 @@ function TopBar({
function Launchpad({ onPrompt, statusDetail, statusLabel }) {
return (
<div className="launchpad">
<div className="hero-mark" aria-hidden="true">
<Sparkle weight="fill" />
</div>
<h1>BlitzKode</h1>
<p className="hero-copy">
Ask for code, debug a failure, refactor a file, or turn a rough idea
into a clean implementation plan.
</p>

<div className="composer-preview">
<div className="launchpad-header">
<div>
<span className="preview-label">Ready for</span>
<strong>focused coding tasks</strong>
<div className="page-kicker">Local coding workspace</div>
<h1>BlitzKode</h1>
</div>
<div className="preview-status">
<ShieldCheck size={18} />
Expand Down Expand Up @@ -770,9 +796,15 @@ function Launchpad({ onPrompt, statusDetail, statusLabel }) {
);
}

function MessageList({ copiedMessageId, messages, messagesRef, onCopy }) {
function MessageList({
copiedMessageId,
messages,
messagesRef,
onCodeCopy,
onCopy,
}) {
return (
<div className="message-list" ref={messagesRef}>
<div className="message-list" ref={messagesRef} onClick={onCodeCopy}>
{messages.map((message) => (
<article className={`message message-${message.role}`} key={message.id}>
<div className="message-rail">
Expand Down
89 changes: 23 additions & 66 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ button {
width: 304px;
height: 100vh;
padding: 22px 14px 18px;
overflow-y: auto;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 26%),
var(--sidebar);
Expand All @@ -91,8 +92,7 @@ button {
padding: 0 8px 22px;
}

.brand-mark,
.hero-mark {
.brand-mark {
display: grid;
place-items: center;
flex: 0 0 auto;
Expand Down Expand Up @@ -474,73 +474,36 @@ button {

.launchpad {
width: min(980px, 100%);
margin-top: 10px;
text-align: center;
}

.hero-mark {
width: 106px;
height: 106px;
margin: 0 auto 18px;
color: #050607;
background: #ffffff;
}

.hero-mark svg {
width: 42px;
height: 42px;
margin-top: 12px;
text-align: left;
}

.launchpad h1 {
max-width: 720px;
margin: 0;
font-size: clamp(50px, 8vw, 84px);
line-height: 0.95;
letter-spacing: -0.065em;
}

.hero-copy {
max-width: 590px;
margin: 22px auto 0;
color: var(--text-soft);
font-size: 17px;
line-height: 1.65;
font-size: clamp(42px, 7vw, 72px);
line-height: 1;
letter-spacing: -0.045em;
}

.composer-preview {
.launchpad-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
max-width: 780px;
min-height: 86px;
margin: 28px auto 26px;
padding: 20px 24px;
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
text-align: left;
}

.preview-label {
display: block;
color: var(--text-muted);
font-size: 13px;
}

.composer-preview strong {
display: block;
margin-top: 4px;
font-size: 20px;
letter-spacing: -0.03em;
margin-bottom: 26px;
}

.preview-status {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--accent-2);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 999px;
min-height: 38px;
padding: 0 13px;
font-size: 13px;
font-weight: 700;
white-space: nowrap;
Expand Down Expand Up @@ -772,6 +735,11 @@ button {
background: #202d3d;
}

.copy-btn:disabled {
cursor: default;
opacity: 0.72;
}

.loading-dots {
display: inline-flex;
gap: 5px;
Expand Down Expand Up @@ -1020,25 +988,14 @@ button {
margin-top: 0;
}

.hero-mark {
width: 82px;
height: 82px;
}

.launchpad h1 {
font-size: 48px;
}

.hero-copy {
margin-top: 16px;
font-size: 15px;
font-size: 44px;
}

.composer-preview {
.launchpad-header {
align-items: flex-start;
flex-direction: column;
min-height: 0;
padding: 16px;
margin-bottom: 18px;
}

.preview-status {
Expand Down
10 changes: 9 additions & 1 deletion frontend/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@ export default defineConfig({
target: 'http://127.0.0.1:7860',
changeOrigin: true,
},
'/info': {
target: 'http://127.0.0.1:7860',
changeOrigin: true,
},
'/search': {
target: 'http://127.0.0.1:7860',
changeOrigin: true,
},
}
},
optimizeDeps: {
include: ['react', 'react-dom', 'marked']
}
})
})
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true
follow_imports = "skip"

[tool.pytest.ini_options]
testpaths = ["tests"]
Loading
Loading