dvc890 commited on
Commit
002a147
·
verified ·
1 Parent(s): 16e4995

Upload 47 files

Browse files
Files changed (2) hide show
  1. pages/AIAssistant.tsx +10 -3
  2. server.js +96 -33
pages/AIAssistant.tsx CHANGED
@@ -70,7 +70,7 @@ export const AIAssistant: React.FC = () => {
70
  const audioChunksRef = useRef<Blob[]>([]);
71
  const audioContextRef = useRef<AudioContext | null>(null);
72
  const currentSourceRef = useRef<AudioBufferSourceNode | null>(null);
73
- const messagesEndRef = useRef<HTMLDivElement>(null); // New: Ref for auto-scrolling
74
 
75
  // Initialize & Load Config
76
  useEffect(() => {
@@ -106,9 +106,16 @@ export const AIAssistant: React.FC = () => {
106
  }
107
  }, [messages]);
108
 
109
- // Auto-scroll effect
110
  useEffect(() => {
111
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
 
 
 
 
 
 
 
112
  }, [messages, isProcessing]);
113
 
114
  const stopPlayback = () => {
 
70
  const audioChunksRef = useRef<Blob[]>([]);
71
  const audioContextRef = useRef<AudioContext | null>(null);
72
  const currentSourceRef = useRef<AudioBufferSourceNode | null>(null);
73
+ const messagesEndRef = useRef<HTMLDivElement>(null);
74
 
75
  // Initialize & Load Config
76
  useEffect(() => {
 
106
  }
107
  }, [messages]);
108
 
109
+ // 1. Initial Scroll on Mount (Wait slightly for layout)
110
  useEffect(() => {
111
+ setTimeout(() => {
112
+ messagesEndRef.current?.scrollIntoView({ behavior: 'auto', block: 'end' });
113
+ }, 100);
114
+ }, []);
115
+
116
+ // 2. Scroll on new messages or processing state change
117
+ useEffect(() => {
118
+ messagesEndRef.current?.scrollIntoView({ behavior: isProcessing ? 'auto' : 'smooth', block: 'end' });
119
  }, [messages, isProcessing]);
120
 
121
  const stopPlayback = () => {
server.js CHANGED
@@ -7,7 +7,6 @@ const {
7
  } = require('./models');
8
 
9
  // Initialize OpenAI (OpenRouter) Client
10
- // We use lazy initialization inside the function to avoid crashes if API Key is missing initially
11
  const OpenAI = require('openai');
12
  let openAIClient = null;
13
 
@@ -205,17 +204,19 @@ function deprioritizeProvider(providerName) {
205
  // If it's already at the end, do nothing
206
  if (activeProviderOrder[activeProviderOrder.length - 1] === providerName) return;
207
 
208
- console.log(`📉 Performance Opt: Deprioritizing ${providerName} due to quota limits.`);
209
  // Move to end
210
  activeProviderOrder = activeProviderOrder.filter(p => p !== providerName).concat(providerName);
211
- console.log(`🔄 New Provider Order: ${activeProviderOrder.join(' -> ')}`);
212
  }
213
 
214
  function isQuotaError(e) {
 
215
  return e.status === 429 || e.status === 503 ||
216
- e.message?.includes('Quota') ||
217
- e.message?.includes('overloaded') ||
218
- e.message?.includes('RESOURCE_EXHAUSTED');
 
219
  }
220
 
221
  // --- INDIVIDUAL PROVIDER CALLERS ---
@@ -229,12 +230,14 @@ async function callGeminiProvider(aiModelObj, baseParams) {
229
  let lastError = null;
230
  for (const modelName of primaryModels) {
231
  try {
 
232
  const currentParams = { ...baseParams, model: modelName };
233
  return await callAIWithRetry(aiModelObj, currentParams, 1);
234
  } catch (e) {
235
  lastError = e;
 
236
  if (isQuotaError(e)) {
237
- console.warn(`⚠️ Gemini Model ${modelName} exhausted. Trying next internal model...`);
238
  continue;
239
  }
240
  throw e; // Fail fast on non-quota errors
@@ -259,7 +262,7 @@ async function callOpenRouterProvider(baseParams) {
259
 
260
  for (const modelName of openRouterModels) {
261
  try {
262
- console.log(`🛡️ Switching to OpenRouter Model: ${modelName}`);
263
  const completion = await openRouter.chat.completions.create({
264
  model: modelName,
265
  messages: openAIMessages,
@@ -275,7 +278,7 @@ async function callOpenRouterProvider(baseParams) {
275
 
276
  } catch (e) {
277
  lastError = e;
278
- console.warn(`⚠️ OpenRouter Model ${modelName} failed.`, e.message);
279
  // Continue to next OpenRouter model
280
  }
281
  }
@@ -296,7 +299,7 @@ async function callGemmaProvider(aiModelObj, baseParams) {
296
  let lastError = null;
297
  for (const modelName of fallbackModels) {
298
  try {
299
- console.log(`🛡️ Switching to Final Backup (Gemma 3): ${modelName}`);
300
  const currentParams = {
301
  ...baseParams,
302
  model: modelName,
@@ -305,7 +308,7 @@ async function callGemmaProvider(aiModelObj, baseParams) {
305
  return await callAIWithRetry(aiModelObj, currentParams, 1);
306
  } catch (e) {
307
  lastError = e;
308
- console.warn(`⚠️ Backup Model ${modelName} failed.`, e.message);
309
  }
310
  }
311
  throw lastError || new Error("Gemma failed");
@@ -314,31 +317,86 @@ async function callGemmaProvider(aiModelObj, baseParams) {
314
  // --- STREAMING PROVIDER HELPERS ---
315
 
316
  async function streamGemini(aiModelObj, baseParams, res) {
317
- const modelName = 'gemini-2.5-flash'; // For streaming, we mostly stick to Flash for speed
318
- const currentParams = { ...baseParams, model: modelName };
319
- const streamResult = await aiModelObj.generateContentStream(currentParams);
320
 
321
- let fullText = '';
322
- for await (const chunk of streamResult) {
323
- const text = chunk.text;
324
- if (text) {
325
- fullText += text;
326
- res.write(`data: ${JSON.stringify({ text: text })}\n\n`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  }
328
  }
329
- return fullText;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  }
331
 
332
  async function streamOpenRouter(baseParams, res) {
333
  const openRouter = getOpenRouter();
334
  if (!openRouter) throw new Error("OpenRouter not configured");
335
 
336
- const openRouterModels = ['qwen/qwen3-coder:free', 'openai/gpt-oss-120b:free'];
 
 
 
 
 
 
337
  const messages = convertGeminiToOpenAI(baseParams);
338
 
339
  let lastError = null;
340
  for (const modelName of openRouterModels) {
341
  try {
 
342
  const stream = await openRouter.chat.completions.create({
343
  model: modelName,
344
  messages: messages,
@@ -356,7 +414,12 @@ async function streamOpenRouter(baseParams, res) {
356
  return fullText;
357
  } catch (e) {
358
  lastError = e;
359
- console.warn(`Stream OpenRouter ${modelName} failed`, e.message);
 
 
 
 
 
360
  }
361
  }
362
  throw lastError || new Error("All OpenRouter streams failed");
@@ -381,9 +444,11 @@ async function generateContentWithSmartFallback(aiModelObj, baseParams) {
381
 
382
  // Constraint: Audio MUST use Gemini.
383
  if (hasAudio) {
 
384
  try {
385
  return await callGeminiProvider(aiModelObj, baseParams);
386
  } catch (e) {
 
387
  if (isQuotaError(e)) {
388
  // Critical: Even if we fail this request, deprioritize Gemini for future TEXT requests
389
  deprioritizeProvider(PROVIDERS.GEMINI);
@@ -414,7 +479,7 @@ async function generateContentWithSmartFallback(aiModelObj, baseParams) {
414
  }
415
  // If it's not a quota error (e.g. invalid input, 400 Bad Request due to image not supported by specific model),
416
  // we typically continue to the next provider to see if they can handle it.
417
- console.warn(`⚠️ ${provider} failed with non-quota error:`, e.message);
418
  }
419
  }
420
 
@@ -422,9 +487,6 @@ async function generateContentWithSmartFallback(aiModelObj, baseParams) {
422
  }
423
 
424
  async function streamContentWithSmartFallback(aiModelObj, baseParams, res) {
425
- // Similar fallback logic but for streaming
426
- // We only support streaming for TEXT generation primarily here.
427
-
428
  // Check for Audio Input (Gemini Only)
429
  let hasAudio = false;
430
  if (baseParams.contents && Array.isArray(baseParams.contents)) {
@@ -448,6 +510,9 @@ async function streamContentWithSmartFallback(aiModelObj, baseParams, res) {
448
  }
449
  }
450
 
 
 
 
451
  for (const provider of activeProviderOrder) {
452
  try {
453
  if (provider === PROVIDERS.GEMINI) {
@@ -455,13 +520,11 @@ async function streamContentWithSmartFallback(aiModelObj, baseParams, res) {
455
  } else if (provider === PROVIDERS.OPENROUTER) {
456
  return await streamOpenRouter(baseParams, res);
457
  } else if (provider === PROVIDERS.GEMMA) {
458
- // Gemma via REST API doesn't support streaming well in this setup easily,
459
- // fallback to non-streaming for Gemma but simulate output?
460
- // For simplicity, we skip Gemma streaming or implement standard fetch.
461
- // Let's just fallback to Gemini logic for now or skip.
462
- continue;
463
  }
464
  } catch (e) {
 
465
  if (isQuotaError(e)) {
466
  deprioritizeProvider(provider);
467
  continue;
@@ -469,7 +532,7 @@ async function streamContentWithSmartFallback(aiModelObj, baseParams, res) {
469
  console.warn(`Streaming ${provider} failed:`, e.message);
470
  }
471
  }
472
- throw new Error('All streaming models unavailable.');
473
  }
474
 
475
  // --- Middleware: Check AI Access ---
 
7
  } = require('./models');
8
 
9
  // Initialize OpenAI (OpenRouter) Client
 
10
  const OpenAI = require('openai');
11
  let openAIClient = null;
12
 
 
204
  // If it's already at the end, do nothing
205
  if (activeProviderOrder[activeProviderOrder.length - 1] === providerName) return;
206
 
207
+ console.log(`📉 [AI Debug] Performance Opt: Deprioritizing ${providerName} due to quota limits.`);
208
  // Move to end
209
  activeProviderOrder = activeProviderOrder.filter(p => p !== providerName).concat(providerName);
210
+ console.log(`🔄 [AI Debug] New Provider Order: ${activeProviderOrder.join(' -> ')}`);
211
  }
212
 
213
  function isQuotaError(e) {
214
+ const msg = e.message || '';
215
  return e.status === 429 || e.status === 503 ||
216
+ msg.includes('Quota') ||
217
+ msg.includes('overloaded') ||
218
+ msg.includes('RESOURCE_EXHAUSTED') ||
219
+ msg.includes('Rate limit');
220
  }
221
 
222
  // --- INDIVIDUAL PROVIDER CALLERS ---
 
230
  let lastError = null;
231
  for (const modelName of primaryModels) {
232
  try {
233
+ console.log(`🚀 [AI Debug] Calling Gemini non-stream: ${modelName}`);
234
  const currentParams = { ...baseParams, model: modelName };
235
  return await callAIWithRetry(aiModelObj, currentParams, 1);
236
  } catch (e) {
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
  }
243
  throw e; // Fail fast on non-quota errors
 
262
 
263
  for (const modelName of openRouterModels) {
264
  try {
265
+ console.log(`🛡️ [AI Debug] Switching to OpenRouter Model: ${modelName}`);
266
  const completion = await openRouter.chat.completions.create({
267
  model: modelName,
268
  messages: openAIMessages,
 
278
 
279
  } catch (e) {
280
  lastError = e;
281
+ console.warn(`⚠️ [AI Debug] OpenRouter Model ${modelName} failed.`, e.message);
282
  // Continue to next OpenRouter model
283
  }
284
  }
 
299
  let lastError = null;
300
  for (const modelName of fallbackModels) {
301
  try {
302
+ console.log(`🛡️ [AI Debug] Switching to Final Backup (Gemma 3): ${modelName}`);
303
  const currentParams = {
304
  ...baseParams,
305
  model: modelName,
 
308
  return await callAIWithRetry(aiModelObj, currentParams, 1);
309
  } catch (e) {
310
  lastError = e;
311
+ console.warn(`⚠️ [AI Debug] Backup Model ${modelName} failed.`, e.message);
312
  }
313
  }
314
  throw lastError || new Error("Gemma failed");
 
317
  // --- STREAMING PROVIDER HELPERS ---
318
 
319
  async function streamGemini(aiModelObj, baseParams, res) {
320
+ // Try multiple Flash models internally for quota resilience
321
+ const models = ['gemini-2.5-flash', 'gemini-2.5-flash-lite'];
 
322
 
323
+ let lastError = null;
324
+ for (const modelName of models) {
325
+ try {
326
+ console.log(`🌊 [AI Debug] STREAMING Gemini model: ${modelName}`);
327
+ const currentParams = { ...baseParams, model: modelName };
328
+ const streamResult = await aiModelObj.generateContentStream(currentParams);
329
+
330
+ let fullText = '';
331
+ for await (const chunk of streamResult) {
332
+ const text = chunk.text;
333
+ if (text) {
334
+ fullText += text;
335
+ res.write(`data: ${JSON.stringify({ text: text })}\n\n`);
336
+ }
337
+ }
338
+ console.log(`✅ [AI Debug] Gemini ${modelName} stream complete.`);
339
+ return fullText; // Success
340
+ } catch (e) {
341
+ lastError = e;
342
+ console.error(`❌ [AI Debug] Gemini Stream Error (${modelName}):`, e.status, e.message);
343
+
344
+ if (isQuotaError(e)) {
345
+ console.warn(`Stream Gemini ${modelName} quota exhausted. Switching...`);
346
+ continue; // Try next internal model
347
+ }
348
+ throw e; // Non-quota error, fail fast
349
  }
350
  }
351
+ throw lastError || new Error("Gemini streaming failed after retrying internal models");
352
+ }
353
+
354
+ async function streamGemma(aiModelObj, baseParams, res) {
355
+ const models = ['gemma-3-27b-it', 'gemma-3-12b-it', 'gemma-3-4b-it'];
356
+ const gemmaConfig = { ...baseParams.config };
357
+ if (gemmaConfig.systemInstruction) delete gemmaConfig.systemInstruction;
358
+
359
+ let lastError = null;
360
+ for (const modelName of models) {
361
+ try {
362
+ console.log(`🛡️ [AI Debug] Streaming Fallback to Gemma: ${modelName}`);
363
+ const currentParams = { ...baseParams, model: modelName, config: gemmaConfig };
364
+ const streamResult = await aiModelObj.generateContentStream(currentParams);
365
+
366
+ let fullText = '';
367
+ for await (const chunk of streamResult) {
368
+ const text = chunk.text;
369
+ if (text) {
370
+ fullText += text;
371
+ res.write(`data: ${JSON.stringify({ text: text })}\n\n`);
372
+ }
373
+ }
374
+ return fullText;
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");
381
  }
382
 
383
  async function streamOpenRouter(baseParams, res) {
384
  const openRouter = getOpenRouter();
385
  if (!openRouter) throw new Error("OpenRouter not configured");
386
 
387
+ // Updated free model list
388
+ const openRouterModels = [
389
+ 'qwen/qwen3-coder:free',
390
+ 'openai/gpt-oss-120b:free',
391
+ 'qwen/qwen3-235b-a22b:free',
392
+ 'tngtech/deepseek-r1t-chimera:free'
393
+ ];
394
  const messages = convertGeminiToOpenAI(baseParams);
395
 
396
  let lastError = null;
397
  for (const modelName of openRouterModels) {
398
  try {
399
+ console.log(`🛡️ [AI Debug] Streaming via OpenRouter: ${modelName}`);
400
  const stream = await openRouter.chat.completions.create({
401
  model: modelName,
402
  messages: messages,
 
414
  return fullText;
415
  } catch (e) {
416
  lastError = e;
417
+ console.warn(`[AI Debug] Stream OpenRouter ${modelName} failed`, e.message);
418
+ if (isQuotaError(e)) {
419
+ // If Rate Limit, break loop to allow fallback to next PROVIDER immediately
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");
 
444
 
445
  // Constraint: Audio MUST use Gemini.
446
  if (hasAudio) {
447
+ console.log("🎤 [AI Debug] Audio detected, forcing Gemini.");
448
  try {
449
  return await callGeminiProvider(aiModelObj, baseParams);
450
  } catch (e) {
451
+ console.error("❌ [AI Debug] Audio Gemini Failed:", e.message);
452
  if (isQuotaError(e)) {
453
  // Critical: Even if we fail this request, deprioritize Gemini for future TEXT requests
454
  deprioritizeProvider(PROVIDERS.GEMINI);
 
479
  }
480
  // If it's not a quota error (e.g. invalid input, 400 Bad Request due to image not supported by specific model),
481
  // we typically continue to the next provider to see if they can handle it.
482
+ console.warn(`⚠️ [AI Debug] ${provider} failed with non-quota error:`, e.message);
483
  }
484
  }
485
 
 
487
  }
488
 
489
  async function streamContentWithSmartFallback(aiModelObj, baseParams, res) {
 
 
 
490
  // Check for Audio Input (Gemini Only)
491
  let hasAudio = false;
492
  if (baseParams.contents && Array.isArray(baseParams.contents)) {
 
510
  }
511
  }
512
 
513
+ let finalError = null;
514
+ console.log(`🚦 [AI Debug] Starting stream with order: ${activeProviderOrder.join(' -> ')}`);
515
+
516
  for (const provider of activeProviderOrder) {
517
  try {
518
  if (provider === PROVIDERS.GEMINI) {
 
520
  } else if (provider === PROVIDERS.OPENROUTER) {
521
  return await streamOpenRouter(baseParams, res);
522
  } else if (provider === PROVIDERS.GEMMA) {
523
+ // Now supports streaming fallback to Gemma
524
+ return await streamGemma(aiModelObj, baseParams, res);
 
 
 
525
  }
526
  } catch (e) {
527
+ finalError = e;
528
  if (isQuotaError(e)) {
529
  deprioritizeProvider(provider);
530
  continue;
 
532
  console.warn(`Streaming ${provider} failed:`, e.message);
533
  }
534
  }
535
+ throw finalError || new Error('All streaming models unavailable.');
536
  }
537
 
538
  // --- Middleware: Check AI Access ---