fix(chat): Gemini client not reading JSON stream correctly
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user