From 9ce0d76f4c48f5ebf0e56190db6a2cd9fb197c24 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:59:49 +0000 Subject: [PATCH 1/2] chore: save uncommitted diff patch for future rebase - Creates `all_changes_patch.diff` with currently staged uncommitted modifications. Co-authored-by: shervinemp <18602406+shervinemp@users.noreply.github.com> --- all_changes_patch.diff | 570 ++++++++++++++++++ app/api/manual-cache/route.ts | 34 ++ app/api/web-search/route.ts | 29 +- components/generation/generating-progress.tsx | 135 +++-- lib/ai/llm.ts | 71 ++- lib/pdf/constants.ts | 6 + lib/pdf/pdf-providers.ts | 70 +++ lib/pdf/types.ts | 2 +- lib/server/resolve-model.ts | 17 +- lib/web-search/searxng.ts | 43 ++ 10 files changed, 931 insertions(+), 46 deletions(-) create mode 100644 all_changes_patch.diff create mode 100644 app/api/manual-cache/route.ts create mode 100644 lib/web-search/searxng.ts diff --git a/all_changes_patch.diff b/all_changes_patch.diff new file mode 100644 index 000000000..a4a6bbf3c --- /dev/null +++ b/all_changes_patch.diff @@ -0,0 +1,570 @@ +diff --git a/app/api/manual-cache/route.ts b/app/api/manual-cache/route.ts +new file mode 100644 +index 0000000..c9afeb8 +--- /dev/null ++++ b/app/api/manual-cache/route.ts +@@ -0,0 +1,34 @@ ++import { NextResponse } from 'next/server'; ++import fs from 'fs'; ++import path from 'path'; ++ ++// Note: Storing in /tmp to work around serverless readonly filesystems ++// However, since serverless instances are ephemeral, ideally you'd use Redis or Vercel KV. ++// We are storing in /tmp and verifying hash structure to prevent Path Traversal. ++ ++export async function POST(req: Request) { ++ try { ++ const { hash, response } = await req.json(); ++ ++ if (!hash || !response) { ++ return NextResponse.json({ error: 'Missing hash or response' }, { status: 400 }); ++ } ++ ++ // Validate hash to be strictly 32 alphanumeric hex chars to prevent path traversal ++ if (!/^[a-fA-F0-9]{32}$/.test(hash)) { ++ return NextResponse.json({ error: 'Invalid hash format' }, { status: 400 }); ++ } ++ ++ // Since serverless is readonly outside /tmp, use /tmp ++ const CACHE_DIR = path.join('/tmp', '.openmaic', 'manual_cache'); ++ if (!fs.existsSync(CACHE_DIR)) { ++ fs.mkdirSync(CACHE_DIR, { recursive: true }); ++ } ++ ++ fs.writeFileSync(path.join(CACHE_DIR, `${hash}.json`), response, 'utf-8'); ++ ++ return NextResponse.json({ success: true }); ++ } catch (error) { ++ return NextResponse.json({ error: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 }); ++ } ++} +diff --git a/app/api/web-search/route.ts b/app/api/web-search/route.ts +index f2ff627..b16d489 100644 +--- a/app/api/web-search/route.ts ++++ b/app/api/web-search/route.ts +@@ -6,6 +6,7 @@ + */ + + import { searchWithTavily, formatSearchResultsAsContext } from '@/lib/web-search/tavily'; ++import { searchWithSearXNG } from '@/lib/web-search/searxng'; + import { resolveWebSearchApiKey } from '@/lib/server/provider-config'; + import { createLogger } from '@/lib/logger'; + import { apiError, apiSuccess } from '@/lib/server/api-response'; +@@ -15,25 +16,35 @@ const log = createLogger('WebSearch'); + export async function POST(req: Request) { + try { + const body = await req.json(); +- const { query, apiKey: clientApiKey } = body as { ++ const { query, apiKey: clientApiKey, providerId, baseUrl } = body as { + query?: string; + apiKey?: string; ++ providerId?: string; ++ baseUrl?: string; + }; + + if (!query || !query.trim()) { + return apiError('MISSING_REQUIRED_FIELD', 400, 'query is required'); + } + +- const apiKey = resolveWebSearchApiKey(clientApiKey); +- if (!apiKey) { +- return apiError( +- 'MISSING_API_KEY', +- 400, +- 'Tavily API key is not configured. Set it in Settings → Web Search or set TAVILY_API_KEY env var.', +- ); ++ let result; ++ if (providerId === 'searxng') { ++ result = await searchWithSearXNG({ ++ query: query.trim(), ++ baseUrl: baseUrl || process.env.SEARXNG_URL || 'http://127.0.0.1:8080/search' ++ }); ++ } else { ++ const apiKey = resolveWebSearchApiKey(clientApiKey); ++ if (!apiKey) { ++ return apiError( ++ 'MISSING_API_KEY', ++ 400, ++ 'Tavily API key is not configured. Set it in Settings → Web Search or set TAVILY_API_KEY env var.', ++ ); ++ } ++ result = await searchWithTavily({ query: query.trim(), apiKey }); + } + +- const result = await searchWithTavily({ query: query.trim(), apiKey }); + const context = formatSearchResultsAsContext(result); + + return apiSuccess({ +diff --git a/components/generation/generating-progress.tsx b/components/generation/generating-progress.tsx +index 639e79d..f76df6a 100644 +--- a/components/generation/generating-progress.tsx ++++ b/components/generation/generating-progress.tsx +@@ -2,8 +2,11 @@ + + import { useEffect, useState } from 'react'; + import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +-import { Loader2, CheckCircle2, XCircle, Circle } from 'lucide-react'; ++import { Loader2, CheckCircle2, XCircle, Circle, Copy, Play } from 'lucide-react'; + import { useI18n } from '@/lib/hooks/use-i18n'; ++import { Button } from '@/components/ui/button'; ++import { Textarea } from '@/components/ui/textarea'; ++import { toast } from 'sonner'; + + interface GeneratingProgressProps { + outlineReady: boolean; // Is outline generation complete? +@@ -62,6 +65,14 @@ export function GeneratingProgress({ + }: GeneratingProgressProps) { + const { t } = useI18n(); + const [dots, setDots] = useState(''); ++ const [manualResponse, setManualResponse] = useState(''); ++ const [isSubmitting, setIsSubmitting] = useState(false); ++ ++ // Extract hash and prompt ++ const isManualIntervention = error?.startsWith('MANUAL_INTERVENTION_REQUIRED|||'); ++ const errorParts = isManualIntervention ? error?.split('|||') : []; ++ const promptHash = (errorParts && errorParts[1]) || ''; ++ const manualPromptText = (errorParts && errorParts[2]) || ''; + + // Animated dots for loading state + useEffect(() => { +@@ -73,12 +84,41 @@ export function GeneratingProgress({ + } + }, [error, firstPageReady]); + ++ const handleCopyPrompt = () => { ++ if (manualPromptText) { ++ navigator.clipboard.writeText(manualPromptText); ++ toast.success("Prompt copied to clipboard"); ++ } ++ }; ++ ++ const handleSubmitManualResponse = async () => { ++ setIsSubmitting(true); ++ try { ++ await fetch('/api/manual-cache', { ++ method: 'POST', ++ headers: { 'Content-Type': 'application/json' }, ++ body: JSON.stringify({ hash: promptHash, response: manualResponse }) ++ }); ++ ++ // Reload the page. The user will click "Generate" again, ++ // but the backend will instantly skip the step using the cache! ++ toast.success("Saved! Please restart the generation."); ++ window.location.reload(); ++ } catch (_e) { ++ toast.error("Failed to save response."); ++ } finally { ++ setIsSubmitting(false); ++ } ++ }; ++ + return ( +
+ + + +- {error ? ( ++ {isManualIntervention ? ( ++ <> Action Required: Gemini Blocked Output ++ ) : error ? ( + <> + + {t('generation.generationFailed')} +@@ -98,40 +138,67 @@ export function GeneratingProgress({ + + + +- {/* Two milestone status items */} +-
+- +- +-
++ {isManualIntervention ? ( ++
++

++ The API blocked this specific prompt. Copy the text, paste it into the Gemini Web App, and paste the JSON result here. ++

++
++