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
+97 -26
View File
@@ -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);
}
}
</style>
</head>
<body>
@@ -244,7 +261,7 @@
</template>
</div>
</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>
<!-- Textarea -->
@@ -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);