diff --git a/vibn-frontend/app/api/chat/route.ts b/vibn-frontend/app/api/chat/route.ts index 53fe661a..0b6d5666 100644 --- a/vibn-frontend/app/api/chat/route.ts +++ b/vibn-frontend/app/api/chat/route.ts @@ -397,9 +397,9 @@ export async function POST(request: Request) { const principal = await requireWorkspacePrincipal(request); if (principal instanceof NextResponse) return principal; - const userRow = await queryOne<{ data: any }>( + const userRow = await queryOne<{ data: { email?: string } }>( `SELECT data FROM fs_users WHERE id = $1 LIMIT 1`, - [principal.userId] + [principal.userId], ); if (!userRow?.data?.email) { return NextResponse.json({ error: "Unauthorized user" }, { status: 401 }); @@ -1056,6 +1056,25 @@ export async function POST(request: Request) { } } + // Last-resort guard: the model produced NO user-facing text and NO + // tools (e.g. a "thinking" turn that returned only reasoning with an + // empty answer part). The tool-tray recovery above doesn't cover this + // case, so without this the user gets a silent blank bubble. Emit a + // short deterministic fallback so every turn says *something*. + if ( + !aborted && + assistantText.trim().length === 0 && + !anyToolsExecuted + ) { + const fallback = + "I didn't produce a response for that — I may have spent the turn " + + "reasoning without writing an answer. Could you rephrase or add a " + + "bit more detail?"; + assistantText = fallback; + assistantTextSegments.push(fallback); + emit({ type: "text", text: fallback }); + } + // Persist final assistant message. We include `textSegments` // alongside the legacy concatenated `content` so the client // can render reloaded threads with the same per-round bubble diff --git a/vibn-frontend/app/api/chat/threads/[id]/route.ts b/vibn-frontend/app/api/chat/threads/[id]/route.ts index 9a68db9a..3af1a36b 100644 --- a/vibn-frontend/app/api/chat/threads/[id]/route.ts +++ b/vibn-frontend/app/api/chat/threads/[id]/route.ts @@ -11,7 +11,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: const principal = await requireWorkspacePrincipal(request); if (principal instanceof NextResponse) return principal; - const userRow = await queryOne<{ data: any }>( + const userRow = await queryOne<{ data: { email?: string } }>( `SELECT data FROM fs_users WHERE id = $1 LIMIT 1`, [principal.userId] ); @@ -42,7 +42,7 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id const principal = await requireWorkspacePrincipal(request); if (principal instanceof NextResponse) return principal; - const userRow = await queryOne<{ data: any }>( + const userRow = await queryOne<{ data: { email?: string } }>( `SELECT data FROM fs_users WHERE id = $1 LIMIT 1`, [principal.userId] ); @@ -64,7 +64,7 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ i const principal = await requireWorkspacePrincipal(request); if (principal instanceof NextResponse) return principal; - const userRow = await queryOne<{ data: any }>( + const userRow = await queryOne<{ data: { email?: string } }>( `SELECT data FROM fs_users WHERE id = $1 LIMIT 1`, [principal.userId] ); diff --git a/vibn-frontend/app/api/chat/threads/route.ts b/vibn-frontend/app/api/chat/threads/route.ts index c18ce52f..cffd62e2 100644 --- a/vibn-frontend/app/api/chat/threads/route.ts +++ b/vibn-frontend/app/api/chat/threads/route.ts @@ -57,7 +57,7 @@ export async function GET(request: Request) { const principal = await requireWorkspacePrincipal(request); if (principal instanceof NextResponse) return principal; - const userRow = await queryOne<{ data: any }>( + const userRow = await queryOne<{ data: { email?: string } }>( `SELECT data FROM fs_users WHERE id = $1 LIMIT 1`, [principal.userId] ); @@ -120,7 +120,7 @@ export async function POST(request: Request) { const principal = await requireWorkspacePrincipal(request); if (principal instanceof NextResponse) return principal; - const userRow = await queryOne<{ data: any }>( + const userRow = await queryOne<{ data: { email?: string } }>( `SELECT data FROM fs_users WHERE id = $1 LIMIT 1`, [principal.userId] ); diff --git a/vibn-frontend/lib/ai/gemini-chat.ts b/vibn-frontend/lib/ai/gemini-chat.ts index d93513a9..c1ec1c75 100644 --- a/vibn-frontend/lib/ai/gemini-chat.ts +++ b/vibn-frontend/lib/ai/gemini-chat.ts @@ -155,6 +155,15 @@ export async function callGeminiChat(opts: { } } + // Empty-answer fallback: Gemini "thinking" responses can spend the whole + // turn emitting `thought` parts and return NO answer part — leaving `text` + // empty. Mirror the OpenAI-compatible adapter (which promotes + // reasoning_content when content is empty) so the user never gets a blank + // bubble. Only promote when there's also no tool call to render. + if (!text.trim() && thoughts.trim() && toolCalls.length === 0) { + text = thoughts.trim(); + } + return { text, thoughts,