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