web search feature added

This commit is contained in:
migavel508 2025-05-14 12:34:13 +05:30
parent dac37b4344
commit 78dd4c677f
8 changed files with 22921 additions and 2 deletions

View File

@ -43,6 +43,7 @@ import { ExpoQrModal } from '~/components/workbench/ExpoQrModal';
import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
import { useStore } from '@nanostores/react';
import { StickToBottom, useStickToBottomContext } from '~/lib/hooks';
import { WebSearch } from './WebSearch.client';
const TEXTAREA_MIN_HEIGHT = 76;
@ -82,6 +83,7 @@ interface BaseChatProps {
clearDeployAlert?: () => void;
data?: JSONValue[] | undefined;
actionRunner?: ActionRunner;
onWebSearchResult?: (result: string) => void;
}
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
@ -120,6 +122,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
clearSupabaseAlert,
data,
actionRunner,
onWebSearchResult,
},
ref,
) => {
@ -590,6 +593,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
<IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>
<div className="i-ph:paperclip text-xl"></div>
</IconButton>
<WebSearch
onSearchResult={(result) => {
if (onWebSearchResult) {
onWebSearchResult(result);
}
}}
disabled={isStreaming}
/>
<IconButton
title="Enhance prompt"
disabled={input.length === 0 || enhancingPrompt}

View File

@ -504,6 +504,11 @@ export const ChatImpl = memo(
Cookies.set('selectedProvider', newProvider.name, { expires: 30 });
};
const handleWebSearchResult = (result: string) => {
setInput(result);
textareaRef.current?.focus();
};
return (
<BaseChat
ref={animationScope}
@ -564,6 +569,7 @@ export const ChatImpl = memo(
deployAlert={deployAlert}
clearDeployAlert={() => workbenchStore.clearDeployAlert()}
data={chatData}
onWebSearchResult={handleWebSearchResult}
/>
);
},

View File

@ -0,0 +1,113 @@
import { useState } from 'react';
import { IconButton } from '~/components/ui/IconButton';
import { toast } from 'react-toastify';
interface WebSearchProps {
onSearchResult: (result: string) => void;
disabled?: boolean;
}
interface WebSearchResponse {
success: boolean;
data?: {
title: string;
description: string;
mainContent: string;
codeBlocks: string[];
relevantLinks: Array<{
url: string;
text: string;
}>;
sourceUrl: string;
};
error?: string;
}
export const WebSearch = ({ onSearchResult, disabled = false }: WebSearchProps) => {
const [isSearching, setIsSearching] = useState(false);
const formatSearchResult = (data: WebSearchResponse['data']) => {
if (!data) return '';
let result = `# Web Search Results from ${data.sourceUrl}\n\n`;
result += `## ${data.title}\n\n`;
if (data.description) {
result += `**Description:** ${data.description}\n\n`;
}
result += `**Main Content:**\n${data.mainContent}\n\n`;
if (data.codeBlocks.length > 0) {
result += `## Code Examples\n\n`;
data.codeBlocks.forEach((block, index) => {
result += `\`\`\`\n${block}\n\`\`\`\n\n`;
});
}
if (data.relevantLinks.length > 0) {
result += `## Relevant Links\n\n`;
data.relevantLinks.forEach(link => {
result += `- [${link.text}](${link.url})\n`;
});
}
return result;
};
const handleWebSearch = async () => {
if (disabled) return;
try {
setIsSearching(true);
const url = window.prompt('Enter URL to search:');
if (!url) {
setIsSearching(false);
return;
}
const formData = new FormData();
formData.append('url', url);
const response = await fetch('/api-web-search', {
method: 'POST',
body: formData,
});
const data = await response.json() as WebSearchResponse;
if (!response.ok) {
throw new Error(data.error || 'Failed to perform web search');
}
if (!data.data) {
throw new Error('No data received from web search');
}
const formattedResult = formatSearchResult(data.data);
onSearchResult(formattedResult);
toast.success('Web search completed successfully');
} catch (error) {
console.error('Web search error:', error);
toast.error('Failed to perform web search: ' + (error instanceof Error ? error.message : 'Unknown error'));
} finally {
setIsSearching(false);
}
};
return (
<IconButton
title="Web Search"
disabled={disabled || isSearching}
onClick={handleWebSearch}
className="transition-all"
>
{isSearching ? (
<div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
) : (
<div className="i-ph:globe text-xl"></div>
)}
</IconButton>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -0,0 +1,97 @@
import { json } from '@remix-run/node';
import type { ActionFunctionArgs } from '@remix-run/node';
export async function action({ request }: ActionFunctionArgs) {
try {
const formData = await request.formData();
const url = formData.get('url') as string;
if (!url) {
return json({ error: 'URL is required' }, { status: 400 });
}
// Add proper headers to handle CORS and content type
const response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
}
});
if (!response.ok) {
throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`);
}
const contentType = response.headers.get('content-type');
if (!contentType?.includes('text/html')) {
throw new Error('URL must point to an HTML page');
}
const html = await response.text();
// Extract title
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
const title = titleMatch ? titleMatch[1].trim() : 'No title found';
// Extract meta description
const descriptionMatch = html.match(/<meta[^>]*name="description"[^>]*content="([^"]*)"[^>]*>/i);
const description = descriptionMatch ? descriptionMatch[1].trim() : '';
// Extract main content
const mainContent = html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
// Extract code blocks
const codeBlocks = html.match(/<pre[^>]*>[\s\S]*?<\/pre>|<code[^>]*>[\s\S]*?<\/code>/gi) || [];
const formattedCodeBlocks = codeBlocks.map(block => {
return block
.replace(/<[^>]+>/g, '')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.trim();
});
// Extract links
const links = html.match(/<a[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi) || [];
const formattedLinks = links.map(link => {
const hrefMatch = link.match(/href="([^"]*)"/i);
const textMatch = link.match(/>([^<]*)</i);
return {
url: hrefMatch ? hrefMatch[1] : '',
text: textMatch ? textMatch[1].trim() : ''
};
});
// Structure the content for code generation
const structuredContent = {
title,
description,
mainContent: mainContent.slice(0, 1000) + '...',
codeBlocks: formattedCodeBlocks,
relevantLinks: formattedLinks.filter(link =>
link.url &&
!link.url.startsWith('#') &&
!link.url.startsWith('javascript:') &&
link.text.trim()
),
sourceUrl: url
};
return json({
success: true,
data: structuredContent
});
} catch (error) {
console.error('Web search error:', error);
return json(
{ error: error instanceof Error ? error.message : 'Unknown error occurred' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,48 @@
import { json } from '@remix-run/node';
import type { ActionFunctionArgs } from '@remix-run/node';
export async function action({ request }: ActionFunctionArgs) {
try {
const formData = await request.formData();
const url = formData.get('url') as string;
if (!url) {
return json({ error: 'URL is required' }, { status: 400 });
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`);
}
const html = await response.text();
// Basic HTML parsing to extract title and content
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
const title = titleMatch ? titleMatch[1].trim() : 'No title found';
// Extract content by removing script and style tags, then getting text content
const content = html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 1000) + '...'; // Limit content length
return json({
success: true,
data: {
title,
content,
url
}
});
} catch (error) {
console.error('Web search error:', error);
return json(
{ error: error instanceof Error ? error.message : 'Unknown error occurred' },
{ status: 500 }
);
}
}

22643
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -151,7 +151,6 @@
"unist-util-visit": "^5.0.0",
"use-debounce": "^10.0.4",
"vite-plugin-node-polyfills": "^0.22.0",
"zod": "^3.24.1",
"zustand": "^5.0.3"
},
"devDependencies": {
@ -160,6 +159,7 @@
"@electron/notarize": "^2.5.0",
"@iconify-json/ph": "^1.2.1",
"@iconify/types": "^2.0.0",
"@mendable/firecrawl-js": "^1.24.0",
"@remix-run/dev": "^2.15.2",
"@remix-run/serve": "^2.15.2",
"@testing-library/jest-dom": "^6.6.3",
@ -199,7 +199,8 @@
"vite-plugin-optimize-css-modules": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.1.7",
"wrangler": "^4.5.1"
"wrangler": "^4.5.1",
"zod": "^3.24.3"
},
"resolutions": {
"@typescript-eslint/utils": "^8.0.0-alpha.30"