Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -82,6 +83,7 @@ interface BaseChatProps {
clearDeployAlert?: () => void;
data?: JSONValue[] | undefined;
actionRunner?: ActionRunner;
onWebSearchResult?: (result: string) => void;
}

export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
Expand Down Expand Up @@ -120,6 +122,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
clearSupabaseAlert,
data,
actionRunner,
onWebSearchResult,
},
ref,
) => {
Expand Down Expand Up @@ -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}
Expand Down
6 changes: 6 additions & 0 deletions app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -564,6 +569,7 @@ export const ChatImpl = memo(
deployAlert={deployAlert}
clearDeployAlert={() => workbenchStore.clearDeployAlert()}
data={chatData}
onWebSearchResult={handleWebSearchResult}
/>
);
},
Expand Down
118 changes: 118 additions & 0 deletions app/components/chat/WebSearch.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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 added app/components/header/logo_roar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 94 additions & 0 deletions app/routes/api-web-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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 });
}
}
47 changes: 47 additions & 0 deletions app/routes/api/web-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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 });
}
}
Empty file added app/types/build.d.ts
Empty file.
Loading