fix(chat): Gemini client not reading JSON stream correctly

This commit is contained in:
2025-12-24 23:00:14 +01:00
parent 908bcce9f3
commit 4e8ebc040c
+96 -25
View File
@@ -11,7 +11,7 @@
--bg-secondary: #f3f4f6; --bg-secondary: #f3f4f6;
--bg-input: #ffffff; --bg-input: #ffffff;
--bg-user-message: #3b82f6; --bg-user-message: #3b82f6;
--bg-code: #f5f5f5; --bg-code: #f0f0f0;
--text-primary: #1f2937; --text-primary: #1f2937;
--text-user: #ffffff; --text-user: #ffffff;
--text-secondary: #6b7280; --text-secondary: #6b7280;
@@ -24,7 +24,7 @@
--bg-secondary: #111827; --bg-secondary: #111827;
--bg-input: #374151; --bg-input: #374151;
--bg-user-message: #2563eb; --bg-user-message: #2563eb;
--bg-code: #0f172a; --bg-code: #242f48;
--text-primary: #f3f4f6; --text-primary: #f3f4f6;
--text-user: #ffffff; --text-user: #ffffff;
--text-secondary: #d1d5db; --text-secondary: #d1d5db;
@@ -227,6 +227,23 @@
[x-cloak] { [x-cloak] {
display: none !important; 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);
}
}
</style> </style>
</head> </head>
<body> <body>
@@ -244,7 +261,7 @@
</template> </template>
</div> </div>
</template> </template>
<div class="message system-message" x-show="isBusy" x-ref="incomingMessage"></div> <div class="message system-message" x-show="isBusy" x-ref="incomingMessage"><span class="spinner"></span></div>
</div> </div>
<!-- Textarea --> <!-- Textarea -->
@@ -298,11 +315,9 @@
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 preach about implications, do not praise excessively.
do not praise excessively. Keep a critical mindset, do not assume blindly.
Keep a critical mindset, 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.
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.
Answer in valid markdown only, no exceptions. Answer in valid markdown only, no exceptions.
` `
.replace(/^\s+/gm, "") .replace(/^\s+/gm, "")
@@ -413,6 +428,7 @@
if (fullResponse) { if (fullResponse) {
this.messages.push({ role: "assistant", content: fullResponse }); this.messages.push({ role: "assistant", content: fullResponse });
window.lastResponse = fullResponse;
} }
} catch (err) { } catch (err) {
const note = err.name === "AbortError" ? "\n\n[Cancelled]" : `\n\n[Error: ${err.message}]`; const note = err.name === "AbortError" ? "\n\n[Cancelled]" : `\n\n[Error: ${err.message}]`;
@@ -495,47 +511,102 @@
role: msg.role === "assistant" ? "model" : "user", role: msg.role === "assistant" ? "model" : "user",
parts: [{ text: msg.content }], parts: [{ text: msg.content }],
})); }));
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}`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ contents, generationConfig: { thinkingConfig: { thinkingLevel: "low" }, ...rest } }), body: JSON.stringify({
contents,
generationConfig: rest,
}),
signal, signal,
}); });
if (!response.ok) { if (!response.ok) {
const text = await response.text(); const text = await response.text();
await onError?.(new Error(`Gemini API Error ${response.status}: ${text}`)); await onError?.(new Error(`Gemini API Error ${response.status}: ${text}`));
return; return;
} }
const reader = response.body.getReader(); const reader = response.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ""; let buffer = "";
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) break; if (done) break;
buffer += decoder.decode(value, { stream: true }); buffer += decoder.decode(value, { stream: true });
let startIdx;
while ((startIdx = buffer.indexOf("{")) !== -1) { let pos = 0;
let endIdx = -1, while (pos < buffer.length) {
depth = 0; // Find start of JSON object
for (let i = startIdx; i < buffer.length; i++) { const start = buffer.indexOf("{", pos);
if (buffer[i] === "{") depth++; if (start === -1) break;
else if (buffer[i] === "}") depth--;
// 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) { if (depth === 0) {
endIdx = i; end = i;
break; break;
} }
} }
if (endIdx === -1) break;
const jsonStr = buffer.substring(startIdx, endIdx + 1);
buffer = buffer.substring(endIdx + 1);
try {
const parsed = JSON.parse(jsonStr);
const content = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
if (content) await onContent(content);
} catch (e) {}
} }
} }
if (end === -1) {
// Incomplete JSON, keep it in buffer
buffer = buffer.slice(start);
break;
}
// Parse and process JSON
try {
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) { } catch (err) {
if (err.name !== "AbortError") await onError?.(err); if (err.name !== "AbortError") await onError?.(err);
} }