From 4e8ebc040c30ac5e5a3768f693a7c4a644b9d532 Mon Sep 17 00:00:00 2001 From: Abdussamet Kocak Date: Wed, 24 Dec 2025 23:00:14 +0100 Subject: [PATCH] fix(chat): Gemini client not reading JSON stream correctly --- chat.html | 123 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 97 insertions(+), 26 deletions(-) diff --git a/chat.html b/chat.html index bcf458e..d3e259c 100644 --- a/chat.html +++ b/chat.html @@ -11,7 +11,7 @@ --bg-secondary: #f3f4f6; --bg-input: #ffffff; --bg-user-message: #3b82f6; - --bg-code: #f5f5f5; + --bg-code: #f0f0f0; --text-primary: #1f2937; --text-user: #ffffff; --text-secondary: #6b7280; @@ -24,7 +24,7 @@ --bg-secondary: #111827; --bg-input: #374151; --bg-user-message: #2563eb; - --bg-code: #0f172a; + --bg-code: #242f48; --text-primary: #f3f4f6; --text-user: #ffffff; --text-secondary: #d1d5db; @@ -227,6 +227,23 @@ [x-cloak] { display: none !important; } + + .spinner { + display: inline-block; + width: 1em; + height: 1em; + border: 3px solid var(--text-secondary); + border-top-color: transparent; + border-radius: 50%; + vertical-align: baseline; + animation: spin 1s ease-in infinite; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } @@ -244,7 +261,7 @@ -
+
@@ -298,11 +315,9 @@ window.env ??= {}; const systemPrompt = ` You are a helpful AI assistant. - Do not preach about implications, - do not praise excessively. - Keep a critical mindset, - do not assume blindly, and challenge my statements when appropriate. - When I ask for code, assume that I am a senior backend developer and do not explain excessively. + Do not preach about implications, do not praise excessively. + Keep a critical mindset, do not assume blindly. + When I ask for code, assume that I am a senior backend developer (do not mention that) and do not explain excessively, but and challenge my statements if I don't sound right, especially in coding. Answer in valid markdown only, no exceptions. ` .replace(/^\s+/gm, "") @@ -413,6 +428,7 @@ if (fullResponse) { this.messages.push({ role: "assistant", content: fullResponse }); + window.lastResponse = fullResponse; } } catch (err) { const note = err.name === "AbortError" ? "\n\n[Cancelled]" : `\n\n[Error: ${err.message}]`; @@ -495,46 +511,101 @@ role: msg.role === "assistant" ? "model" : "user", parts: [{ text: msg.content }], })); + try { const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?key=${apiKey}`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ contents, generationConfig: { thinkingConfig: { thinkingLevel: "low" }, ...rest } }), + body: JSON.stringify({ + contents, + generationConfig: rest, + }), signal, }); + if (!response.ok) { const text = await response.text(); await onError?.(new Error(`Gemini API Error ${response.status}: ${text}`)); return; } + const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; + while (true) { const { done, value } = await reader.read(); if (done) break; + buffer += decoder.decode(value, { stream: true }); - let startIdx; - while ((startIdx = buffer.indexOf("{")) !== -1) { - let endIdx = -1, - depth = 0; - for (let i = startIdx; i < buffer.length; i++) { - if (buffer[i] === "{") depth++; - else if (buffer[i] === "}") depth--; - if (depth === 0) { - endIdx = i; - break; + + let pos = 0; + while (pos < buffer.length) { + // Find start of JSON object + const start = buffer.indexOf("{", pos); + if (start === -1) break; + + // Find matching closing brace + let depth = 0; + let inString = false; + let escaped = false; + let end = -1; + + for (let i = start; i < buffer.length; i++) { + const ch = buffer[i]; + + if (escaped) { + escaped = false; + continue; + } + + if (ch === "\\") { + escaped = true; + continue; + } + + if (ch === '"') { + inString = !inString; + continue; + } + + if (!inString) { + if (ch === "{") depth++; + else if (ch === "}") { + depth--; + if (depth === 0) { + end = i; + break; + } + } } } - if (endIdx === -1) break; - const jsonStr = buffer.substring(startIdx, endIdx + 1); - buffer = buffer.substring(endIdx + 1); + + if (end === -1) { + // Incomplete JSON, keep it in buffer + buffer = buffer.slice(start); + break; + } + + // Parse and process JSON try { - const parsed = JSON.parse(jsonStr); - const content = parsed.candidates?.[0]?.content?.parts?.[0]?.text; - if (content) await onContent(content); - } catch (e) {} + const json = JSON.parse(buffer.slice(start, end + 1)); + const parts = json.candidates?.[0]?.content?.parts || []; + const text = parts + .filter((p) => p.text) + .map((p) => p.text) + .join(""); + + if (text) await onContent(text); + } catch (e) { + // Ignore parse errors + } + + pos = end + 1; } + + // Keep only unparsed remainder + buffer = buffer.slice(pos); } } catch (err) { if (err.name !== "AbortError") await onError?.(err);