Spaces:
Running
Running
Upload 47 files
Browse files- pages/AIAssistant.tsx +8 -8
- server.js +9 -8
pages/AIAssistant.tsx
CHANGED
|
@@ -461,6 +461,14 @@ export const AIAssistant: React.FC = () => {
|
|
| 461 |
</ReactMarkdown>
|
| 462 |
</div>
|
| 463 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
{msg.audio && (
|
| 465 |
<button onClick={() => playPCMAudio(msg.audio!)} className="mt-2 flex items-center gap-2 text-xs bg-blue-50 text-blue-600 px-3 py-1.5 rounded-full hover:bg-blue-100 border border-blue-100 transition-colors w-fit">
|
| 466 |
<Volume2 size={14}/> 播放语音
|
|
@@ -469,14 +477,6 @@ export const AIAssistant: React.FC = () => {
|
|
| 469 |
</div>
|
| 470 |
</div>
|
| 471 |
))}
|
| 472 |
-
{isProcessing && messages[messages.length - 1]?.role === 'user' && (
|
| 473 |
-
<div className="flex gap-3">
|
| 474 |
-
<div className="w-10 h-10 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center">
|
| 475 |
-
<Loader2 className="animate-spin" size={20}/>
|
| 476 |
-
</div>
|
| 477 |
-
<div className="text-sm text-gray-400 self-center">AI 正在思考...</div>
|
| 478 |
-
</div>
|
| 479 |
-
)}
|
| 480 |
<div ref={messagesEndRef} />
|
| 481 |
</div>
|
| 482 |
|
|
|
|
| 461 |
</ReactMarkdown>
|
| 462 |
</div>
|
| 463 |
|
| 464 |
+
{/* Loading State INSIDE Bubble */}
|
| 465 |
+
{msg.role === 'model' && !msg.text && isProcessing && (
|
| 466 |
+
<div className="flex items-center gap-2 text-gray-400 py-1">
|
| 467 |
+
<Loader2 className="animate-spin" size={14}/>
|
| 468 |
+
<span className="text-xs">思考中...</span>
|
| 469 |
+
</div>
|
| 470 |
+
)}
|
| 471 |
+
|
| 472 |
{msg.audio && (
|
| 473 |
<button onClick={() => playPCMAudio(msg.audio!)} className="mt-2 flex items-center gap-2 text-xs bg-blue-50 text-blue-600 px-3 py-1.5 rounded-full hover:bg-blue-100 border border-blue-100 transition-colors w-fit">
|
| 474 |
<Volume2 size={14}/> 播放语音
|
|
|
|
| 477 |
</div>
|
| 478 |
</div>
|
| 479 |
))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
<div ref={messagesEndRef} />
|
| 481 |
</div>
|
| 482 |
|
server.js
CHANGED
|
@@ -216,7 +216,8 @@ function isQuotaError(e) {
|
|
| 216 |
msg.includes('Quota') ||
|
| 217 |
msg.includes('overloaded') ||
|
| 218 |
msg.includes('RESOURCE_EXHAUSTED') ||
|
| 219 |
-
msg.includes('Rate limit')
|
|
|
|
| 220 |
}
|
| 221 |
|
| 222 |
// --- INDIVIDUAL PROVIDER CALLERS ---
|
|
@@ -237,6 +238,7 @@ async function callGeminiProvider(aiModelObj, baseParams) {
|
|
| 237 |
lastError = e;
|
| 238 |
console.error(`⚠️ [AI Debug] Gemini ${modelName} Error:`, e.status, e.message);
|
| 239 |
if (isQuotaError(e)) {
|
|
|
|
| 240 |
console.warn(`⚠️ [AI Debug] Gemini ${modelName} exhausted. Trying next internal model...`);
|
| 241 |
continue;
|
| 242 |
}
|
|
@@ -342,8 +344,9 @@ async function streamGemini(aiModelObj, baseParams, res) {
|
|
| 342 |
console.error(`❌ [AI Debug] Gemini Stream Error (${modelName}):`, e.status, e.message);
|
| 343 |
|
| 344 |
if (isQuotaError(e)) {
|
| 345 |
-
|
| 346 |
-
|
|
|
|
| 347 |
}
|
| 348 |
throw e; // Non-quota error, fail fast
|
| 349 |
}
|
|
@@ -375,6 +378,7 @@ async function streamGemma(aiModelObj, baseParams, res) {
|
|
| 375 |
} catch (e) {
|
| 376 |
lastError = e;
|
| 377 |
console.warn(`Stream Gemma ${modelName} failed: ${e.message}`);
|
|
|
|
| 378 |
}
|
| 379 |
}
|
| 380 |
throw lastError || new Error("Gemma streaming failed");
|
|
@@ -415,11 +419,8 @@ async function streamOpenRouter(baseParams, res) {
|
|
| 415 |
} catch (e) {
|
| 416 |
lastError = e;
|
| 417 |
console.warn(`[AI Debug] Stream OpenRouter ${modelName} failed`, e.message);
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
// instead of trying all OpenRouter models which share quota
|
| 421 |
-
throw e;
|
| 422 |
-
}
|
| 423 |
}
|
| 424 |
}
|
| 425 |
throw lastError || new Error("All OpenRouter streams failed");
|
|
|
|
| 216 |
msg.includes('Quota') ||
|
| 217 |
msg.includes('overloaded') ||
|
| 218 |
msg.includes('RESOURCE_EXHAUSTED') ||
|
| 219 |
+
msg.includes('Rate limit') ||
|
| 220 |
+
msg.includes('credits');
|
| 221 |
}
|
| 222 |
|
| 223 |
// --- INDIVIDUAL PROVIDER CALLERS ---
|
|
|
|
| 238 |
lastError = e;
|
| 239 |
console.error(`⚠️ [AI Debug] Gemini ${modelName} Error:`, e.status, e.message);
|
| 240 |
if (isQuotaError(e)) {
|
| 241 |
+
// IMPORTANT: Continue to next internal model, do not throw yet
|
| 242 |
console.warn(`⚠️ [AI Debug] Gemini ${modelName} exhausted. Trying next internal model...`);
|
| 243 |
continue;
|
| 244 |
}
|
|
|
|
| 344 |
console.error(`❌ [AI Debug] Gemini Stream Error (${modelName}):`, e.status, e.message);
|
| 345 |
|
| 346 |
if (isQuotaError(e)) {
|
| 347 |
+
// IMPORTANT: Continue to next internal model
|
| 348 |
+
console.warn(`[AI Debug] Stream Gemini ${modelName} quota exhausted. Switching to next internal model...`);
|
| 349 |
+
continue;
|
| 350 |
}
|
| 351 |
throw e; // Non-quota error, fail fast
|
| 352 |
}
|
|
|
|
| 378 |
} catch (e) {
|
| 379 |
lastError = e;
|
| 380 |
console.warn(`Stream Gemma ${modelName} failed: ${e.message}`);
|
| 381 |
+
// Continue to next gemma model
|
| 382 |
}
|
| 383 |
}
|
| 384 |
throw lastError || new Error("Gemma streaming failed");
|
|
|
|
| 419 |
} catch (e) {
|
| 420 |
lastError = e;
|
| 421 |
console.warn(`[AI Debug] Stream OpenRouter ${modelName} failed`, e.message);
|
| 422 |
+
// CRITICAL FIX: Do NOT throw here. Continue loop to try next OpenRouter model.
|
| 423 |
+
// Only throw if loop finishes.
|
|
|
|
|
|
|
|
|
|
| 424 |
}
|
| 425 |
}
|
| 426 |
throw lastError || new Error("All OpenRouter streams failed");
|