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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Get your Firecrawl API Key here: https://www.firecrawl.dev/app/api-keys
FIRECRAWL_KEY=****

# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
AUTH_SECRET=****

Expand Down
6 changes: 6 additions & 0 deletions app/(chat)/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import { fetchModels } from 'tokenlens/fetch';
import { getUsage } from 'tokenlens/helpers';
import type { ModelCatalog } from 'tokenlens/core';
import type { AppUsage } from '@/lib/usage';
import { searchWeb } from '@/lib/ai/tools/web-search';
import { scrapeSite } from '@/lib/ai/tools/scrape-site';

export const maxDuration = 60;

Expand Down Expand Up @@ -185,13 +187,17 @@ export async function POST(request: Request) {
? []
: [
'getWeather',
'searchWeb',
'scrapeSite',
'createDocument',
'updateDocument',
'requestSuggestions',
],
experimental_transform: smoothStream({ chunking: 'word' }),
tools: {
getWeather,
searchWeb,
scrapeSite,
createDocument: createDocument({ session, dataStream }),
updateDocument: updateDocument({ session, dataStream }),
requestSuggestions: requestSuggestions({
Expand Down
16 changes: 8 additions & 8 deletions components/elements/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ function InfoRow({
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">{label}</span>
<div className="flex items-center gap-2 font-mono">
<span className="text-right min-w-[4ch]">
<span className='min-w-[4ch] text-right'>
{tokens === undefined ? '—' : tokens.toLocaleString()}
</span>
{costText !== undefined && costText !== null && !isNaN(parseFloat(costText)) && (
{costText !== undefined && costText !== null && !Number.isNaN(Number.parseFloat(costText)) && (
<span className="text-muted-foreground">
${parseFloat(costText).toFixed(6)}
${Number.parseFloat(costText).toFixed(6)}
</span>
)}
</div>
Expand All @@ -111,7 +111,7 @@ export const Context = ({ className, usage, ...props }: ContextProps) => {
<DropdownMenuTrigger asChild>
<button
className={cn(
'inline-flex items-center gap-1 select-none rounded-md text-sm',
'inline-flex select-none items-center gap-1 rounded-md text-sm',
'cursor-pointer bg-background text-foreground',
className,
)}
Expand Down Expand Up @@ -165,13 +165,13 @@ export const Context = ({ className, usage, ...props }: ContextProps) => {
{usage?.costUSD?.totalUSD !== undefined && (
<>
<Separator className="mt-1" />
<div className="flex justify-between items-center pt-1 text-xs">
<div className='flex items-center justify-between pt-1 text-xs'>
<span className="text-muted-foreground">Total cost</span>
<div className="flex items-center gap-2 font-mono">
<span className="text-right min-w-[4ch]"></span>
<span className='min-w-[4ch] text-right' />
<span>
{!isNaN(parseFloat(usage.costUSD.totalUSD.toString()))
? `$${parseFloat(usage.costUSD.totalUSD.toString()).toFixed(6)}`
{!Number.isNaN(Number.parseFloat(usage.costUSD.totalUSD.toString()))
? `$${Number.parseFloat(usage.costUSD.totalUSD.toString()).toFixed(6)}`
: '—'
}
</span>
Expand Down
18 changes: 18 additions & 0 deletions components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,24 @@ const PurePreviewMessage = ({
</Tool>
);
}

if (type === 'tool-searchWeb' || type === 'tool-scrapeSite') {
const { toolCallId, state } = part;
return (
<Tool key={toolCallId} defaultOpen={true}>
<ToolHeader type={type} state={state} />
<ToolContent>
{state === 'input-available' && <ToolInput input={part.input} />}
{state === 'output-available' && (
<ToolOutput
output={<pre className="whitespace-pre-wrap text-sm">{JSON.stringify(part.output, null, 2)}</pre>}
errorText={undefined}
/>
)}
</ToolContent>
</Tool>
);
}

if (type === 'tool-createDocument') {
const { toolCallId } = part;
Expand Down
6 changes: 3 additions & 3 deletions components/model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ export function ModelSelector({
>
<button
type="button"
className="flex flex-row gap-2 justify-between items-center w-full group/item sm:gap-4"
className='group/item flex w-full flex-row items-center justify-between gap-2 sm:gap-4'
>
<div className="flex flex-col gap-1 items-start">
<div className='flex flex-col items-start gap-1'>
<div className="text-sm sm:text-base">{chatModel.name}</div>
<div className="text-xs line-clamp-2 text-muted-foreground">
<div className='line-clamp-2 text-muted-foreground text-xs'>
{chatModel.description}
</div>
</div>
Expand Down
28 changes: 14 additions & 14 deletions components/multimodal-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,15 +243,15 @@ function PureMultimodalInput({
}, [status, scrollToBottom]);

return (
<div className="flex relative flex-col gap-4 w-full">
<div className='relative flex w-full flex-col gap-4'>
<AnimatePresence>
{!isAtBottom && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
className="absolute -top-12 left-1/2 z-50 -translate-x-1/2"
className='-top-12 -translate-x-1/2 absolute left-1/2 z-50'
>
<Button
data-testid="scroll-to-bottom-button"
Expand Down Expand Up @@ -289,7 +289,7 @@ function PureMultimodalInput({
/>

<PromptInput
className="p-3 rounded-xl border transition-all duration-200 border-border bg-background shadow-xs focus-within:border-border hover:border-muted-foreground/50"
className='rounded-xl border border-border bg-background p-3 shadow-xs transition-all duration-200 focus-within:border-border hover:border-muted-foreground/50'
onSubmit={(event) => {
event.preventDefault();
if (status !== 'ready') {
Expand All @@ -302,7 +302,7 @@ function PureMultimodalInput({
{(attachments.length > 0 || uploadQueue.length > 0) && (
<div
data-testid="attachments-preview"
className="flex overflow-x-scroll flex-row gap-2 items-end"
className='flex flex-row items-end gap-2 overflow-x-scroll'
>
{attachments.map((attachment) => (
<PreviewAttachment
Expand Down Expand Up @@ -332,7 +332,7 @@ function PureMultimodalInput({
))}
</div>
)}
<div className="flex flex-row gap-1 items-start sm:gap-2">
<div className='flex flex-row items-start gap-1 sm:gap-2'>
<PromptInputTextarea
data-testid="multimodal-input"
ref={textareaRef}
Expand All @@ -342,7 +342,7 @@ function PureMultimodalInput({
minHeight={44}
maxHeight={200}
disableAutoResize={true}
className="grow resize-none border-0! p-2 border-none! bg-transparent text-sm outline-none ring-0 [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 [&::-webkit-scrollbar]:hidden"
className='grow resize-none border-0! border-none! bg-transparent p-2 text-sm outline-none ring-0 [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 [&::-webkit-scrollbar]:hidden'
rows={1}
autoFocus
/>{' '}
Expand All @@ -364,7 +364,7 @@ function PureMultimodalInput({
<PromptInputSubmit
status={status}
disabled={!input.trim() || uploadQueue.length > 0}
className="rounded-full transition-colors duration-200 size-8 bg-primary text-primary-foreground hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground"
className='size-8 rounded-full bg-primary text-primary-foreground transition-colors duration-200 hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground'
>
<ArrowUpIcon size={14} />
</PromptInputSubmit>
Expand Down Expand Up @@ -403,7 +403,7 @@ function PureAttachmentsButton({
return (
<Button
data-testid="attachments-button"
className="p-1 h-8 rounded-lg transition-colors aspect-square hover:bg-accent"
className='aspect-square h-8 rounded-lg p-1 transition-colors hover:bg-accent'
onClick={(event) => {
event.preventDefault();
fileInputRef.current?.click();
Expand Down Expand Up @@ -451,10 +451,10 @@ function PureModelSelectorCompact({
>
<SelectPrimitive.Trigger
type="button"
className="flex gap-2 items-center px-2 h-8 rounded-lg border-0 shadow-none transition-colors bg-background text-foreground hover:bg-accent focus:outline-none focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
className='flex h-8 items-center gap-2 rounded-lg border-0 bg-background px-2 text-foreground shadow-none transition-colors hover:bg-accent focus:outline-none focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0'
>
<CpuIcon size={16} />
<span className="hidden text-xs font-medium sm:block">
<span className='hidden font-medium text-xs sm:block'>
{selectedModel?.name}
</span>
<ChevronDownIcon size={16} />
Expand All @@ -467,9 +467,9 @@ function PureModelSelectorCompact({
value={model.name}
className="px-3 py-2 text-xs"
>
<div className="flex flex-col flex-1 gap-1 min-w-0">
<div className="text-xs font-medium truncate">{model.name}</div>
<div className="text-[10px] text-muted-foreground truncate leading-tight">
<div className='flex min-w-0 flex-1 flex-col gap-1'>
<div className='truncate font-medium text-xs'>{model.name}</div>
<div className='truncate text-[10px] text-muted-foreground leading-tight'>
{model.description}
</div>
</div>
Expand All @@ -493,7 +493,7 @@ function PureStopButton({
return (
<Button
data-testid="stop-button"
className="p-1 rounded-full transition-colors duration-200 size-7 bg-foreground text-background hover:bg-foreground/90 disabled:bg-muted disabled:text-muted-foreground"
className='size-7 rounded-full bg-foreground p-1 text-background transition-colors duration-200 hover:bg-foreground/90 disabled:bg-muted disabled:text-muted-foreground'
onClick={(event) => {
event.preventDefault();
stop();
Expand Down
2 changes: 1 addition & 1 deletion components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pr-8 pl-3 text-sm outline-hidden hover:bg-muted/50 focus:bg-muted data-[state=checked]:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50 transition-colors',
'relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pr-8 pl-3 text-sm outline-hidden transition-colors hover:bg-muted/50 focus:bg-muted data-disabled:pointer-events-none data-[state=checked]:bg-muted data-disabled:opacity-50',
className,
)}
{...props}
Expand Down
2 changes: 1 addition & 1 deletion components/visibility-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function VisibilitySelector({
<Button
data-testid="visibility-selector"
variant="outline"
className="hidden h-8 md:flex md:h-fit md:px-2 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
className='hidden h-8 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 md:flex md:h-fit md:px-2'
>
{selectedVisibility?.icon}
<span className="md:sr-only">{selectedVisibility?.label}</span>
Expand Down
19 changes: 19 additions & 0 deletions lib/ai/tools/scrape-site.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { firecrawl } from '@/lib/firecrawl';
import { tool } from 'ai';
import { z } from 'zod';

export const scrapeSite = tool({
description:
'Get the current HTML of a website to understand its contents (https://example.com)',
inputSchema: z.object({
url: z.string().url(),
}),
execute: async ({ url }) => {
try {
const response = await firecrawl.scrape(url);
return response;
} catch (error) {
throw new Error(`Failed to scrape URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
},
});
20 changes: 20 additions & 0 deletions lib/ai/tools/web-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { firecrawl } from '@/lib/firecrawl';
import { tool } from 'ai';
import { z } from 'zod';

export const searchWeb = tool({
description: 'Search the web for any query',
inputSchema: z.object({
query: z.string(),
}),
execute: async ({ query }) => {
try {
const response = await firecrawl.search(query, {
scrapeOptions: { formats: ['markdown'] },
});
return response;
} catch (error) {
throw new Error(`Failed to search the web: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
},
});
9 changes: 9 additions & 0 deletions lib/firecrawl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Firecrawl from '@mendable/firecrawl-js';

const firecrawlKey = process.env.FIRECRAWL_KEY

if(!firecrawlKey) {
throw new Error("Firecrawl API Key missing")
}

export const firecrawl = new Firecrawl({ apiKey: firecrawlKey });
6 changes: 6 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { AppUsage } from './usage';

import type { ArtifactKind } from '@/components/artifact';
import type { Suggestion } from './db/schema';
import type { scrapeSite } from './ai/tools/scrape-site';
import type { searchWeb } from './ai/tools/web-search';

export type DataPart = { type: 'append-message'; message: string };

Expand All @@ -18,6 +20,8 @@ export const messageMetadataSchema = z.object({
export type MessageMetadata = z.infer<typeof messageMetadataSchema>;

type weatherTool = InferUITool<typeof getWeather>;
type scrapeSiteTool = InferUITool<typeof scrapeSite>;
type searchWebTool = InferUITool<typeof searchWeb>
type createDocumentTool = InferUITool<ReturnType<typeof createDocument>>;
type updateDocumentTool = InferUITool<ReturnType<typeof updateDocument>>;
type requestSuggestionsTool = InferUITool<
Expand All @@ -26,6 +30,8 @@ type requestSuggestionsTool = InferUITool<

export type ChatTools = {
getWeather: weatherTool;
searchWeb: searchWebTool;
scrapeSite: scrapeSiteTool;
createDocument: createDocumentTool;
updateDocument: updateDocumentTool;
requestSuggestions: requestSuggestionsTool;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.35.3",
"@icons-pack/react-simple-icons": "^13.7.0",
"@mendable/firecrawl-js": "^4.3.5",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.200.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
Expand Down
Loading