Template
Improve chat navigation, model selection, and streaming
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
# Copy this file to .env.local, then run yarn install and yarn dev.
|
||||||
|
# Required for chat responses. This key is used only by the server.
|
||||||
|
OPENROUTER_API_KEY=
|
||||||
|
|
||||||
|
# Optional initial model ID from lib/openrouter.ts; users can change it in the composer.
|
||||||
|
OPENROUTER_MODEL=
|
||||||
|
|
||||||
|
# Optional: enables web search and page fetching when configured.
|
||||||
|
FIRECRAWL_API_KEY=
|
||||||
+3
-2
@@ -1,8 +1,9 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules/*
|
node_modules/
|
||||||
.next
|
.next
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.env.local
|
.env*
|
||||||
|
!.env.example
|
||||||
ai/
|
ai/
|
||||||
vercel-ai/
|
vercel-ai/
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
nodeLinker: node-modules
|
||||||
+23
-30
@@ -4,16 +4,20 @@ import {
|
|||||||
stepCountIs,
|
stepCountIs,
|
||||||
pruneMessages,
|
pruneMessages,
|
||||||
} from 'ai';
|
} from 'ai';
|
||||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
import { getChatModel } from '@/services/chat/model';
|
||||||
import { tools } from '@/chat.config';
|
import { getChatInstructions } from '@/services/chat/instructions';
|
||||||
|
import { chatTools } from '@/services/chat/tools';
|
||||||
|
import { isOpenRouterModelId } from '@/lib/openrouter';
|
||||||
|
|
||||||
const openrouter = createOpenRouter({
|
export const maxDuration = 300;
|
||||||
apiKey: process.env.OPENROUTER_API_KEY,
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const { messages } = await request.json();
|
const { messages, modelId } = await request.json();
|
||||||
|
|
||||||
|
if (modelId !== undefined && !isOpenRouterModelId(modelId)) {
|
||||||
|
return Response.json({ error: 'Select a supported model' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
if (!process.env.OPENROUTER_API_KEY) {
|
if (!process.env.OPENROUTER_API_KEY) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
@@ -22,30 +26,11 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = openrouter(
|
const model = getChatModel(modelId);
|
||||||
process.env.OPENROUTER_MODEL || 'openai/gpt-5.6-luna-pro'
|
|
||||||
);
|
|
||||||
|
|
||||||
const webSearchEnabled = Boolean(process.env.FIRECRAWL_API_KEY);
|
const webSearchEnabled = Boolean(process.env.FIRECRAWL_API_KEY);
|
||||||
|
|
||||||
const instructions = webSearchEnabled
|
const instructions = getChatInstructions(webSearchEnabled);
|
||||||
? `You are an expert ChatGPT Agent with real-time access to the internet.
|
|
||||||
|
|
||||||
Your capabilities:
|
|
||||||
- Search the web using Firecrawl to find up-to-date information
|
|
||||||
- Fetch and read full web pages to extract detailed content
|
|
||||||
- Synthesize information from multiple sources into clear, accurate answers
|
|
||||||
- Cite your sources inline so the user can verify information
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
- Always search the web before answering questions that require current or factual information
|
|
||||||
- Use multiple searches when needed to get comprehensive answers
|
|
||||||
- Cross-reference sources for accuracy
|
|
||||||
- Structure your responses clearly with headings, bullet points, and citations where appropriate
|
|
||||||
- When fetching pages, extract only the relevant information
|
|
||||||
- Be transparent about the sources you used
|
|
||||||
- Today's date is ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}`
|
|
||||||
: `You are a helpful AI research assistant. Answer clearly and accurately. You do not have live web access, so be transparent when a question requires current information. Today's date is ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}.`;
|
|
||||||
|
|
||||||
const convertedMessages = await convertToModelMessages(messages);
|
const convertedMessages = await convertToModelMessages(messages);
|
||||||
|
|
||||||
@@ -55,12 +40,16 @@ Guidelines:
|
|||||||
toolCalls: 'none',
|
toolCalls: 'none',
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await streamText({
|
const result = streamText({
|
||||||
model,
|
model,
|
||||||
instructions,
|
instructions,
|
||||||
messages: prunedMessages,
|
messages: prunedMessages,
|
||||||
tools: webSearchEnabled ? tools : {},
|
abortSignal: request.signal,
|
||||||
|
tools: webSearchEnabled ? chatTools : {},
|
||||||
stopWhen: stepCountIs(20),
|
stopWhen: stepCountIs(20),
|
||||||
|
onError: ({ error }) => {
|
||||||
|
if (!request.signal.aborted) console.error('Chat stream error:', error);
|
||||||
|
},
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
openrouter: {
|
openrouter: {
|
||||||
reasoning: {
|
reasoning: {
|
||||||
@@ -71,7 +60,11 @@ Guidelines:
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.toUIMessageStreamResponse({ sendReasoning: true });
|
return result.toUIMessageStreamResponse({
|
||||||
|
sendReasoning: true,
|
||||||
|
headers: { 'Cache-Control': 'no-cache, no-transform' },
|
||||||
|
onError: () => 'The response was interrupted. Please try again.',
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Chat API error:', error);
|
console.error('Chat API error:', error);
|
||||||
return new Response(
|
return new Response(
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<!-- Generated from RiOpenaiLine in @remixicon/react. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><rect width="64" height="64" rx="16" fill="#000000"></rect><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="44" height="44" fill="#ffffff" x="10" y="10" class="remixicon "><path d="M11 4C11.6487 4 12.2498 4.20569 12.7411 4.55646C11.4486 5.22914 10.2602 6.07623 8.99996 6.80385L9.99996 8.5359C11.575 7.62654 13.0517 6.48541 14.7608 5.83037C16.1134 5.31198 17.681 5.83976 18.4282 7.13398C18.7526 7.69582 18.8749 8.31917 18.8168 8.92006C17.588 8.13708 16.2602 7.53146 15 6.80385L14 8.5359C15.575 9.44525 17.3016 10.1536 18.7234 11.3061C19.8487 12.2183 20.1754 13.8398 19.4282 15.134C19.1037 15.696 18.6249 16.1137 18.0752 16.3638C18.1385 14.9081 18 13.4553 18 12H16C16 13.8184 16.2498 15.6685 15.9626 17.4758C15.7353 18.9063 14.4944 20 13 20C12.351 20 11.7498 19.7942 11.2584 19.4432C12.5508 18.7701 13.7396 17.9238 15 17.1962L14 15.4641C12.4252 16.3733 10.9478 17.5147 9.23912 18.1696C7.88657 18.688 6.31898 18.1602 5.57176 16.866C5.24729 16.304 5.12493 15.6805 5.18316 15.0794C6.41224 15.8621 7.73964 16.4685 8.99997 17.1962L9.99997 15.4641C8.42517 14.5549 6.69801 13.8462 5.27649 12.6939C4.15128 11.7817 3.82455 10.1602 4.57176 8.86602C4.89624 8.30402 5.37506 7.88628 5.9247 7.63618C5.86145 9.09192 5.99997 10.5447 5.99997 12H7.99997C7.99997 10.1816 7.75013 8.33148 8.03733 6.52422C8.26467 5.09369 9.50554 4 11 4ZM14.7925 3.74171C13.8765 2.67659 12.5176 2 11 2C8.7782 2 6.89621 3.44833 6.24435 5.45243C4.86393 5.71314 3.59851 6.55175 2.83971 7.86602C1.72883 9.79013 2.04213 12.1442 3.4518 13.7107C2.98737 15.0366 3.08092 16.5518 3.83971 17.866C4.95059 19.7901 7.14589 20.6958 9.20742 20.2583C10.1234 21.3234 11.4824 22 13 22C15.2217 22 17.1037 20.5517 17.7556 18.5476C19.136 18.2869 20.4014 17.4482 21.1602 16.134C22.2711 14.2099 21.9578 11.8559 20.5481 10.2893C21.0126 8.96345 20.919 7.44825 20.1602 6.13398C19.0493 4.20987 16.854 3.30419 14.7925 3.74171Z"></path></svg></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
+21
-2
@@ -3,9 +3,28 @@ import type { Metadata } from 'next';
|
|||||||
import { ThemeProvider } from '@/components/theme-provider';
|
import { ThemeProvider } from '@/components/theme-provider';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
|
const title = 'ChatGPT Agent | AI Chat & Research Assistant';
|
||||||
|
const description =
|
||||||
|
'Ask questions, explore ideas, and get clear AI answers with streaming responses, optional web research, file attachments, and saved conversations.';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'ChatGPT Agent',
|
applicationName: 'ChatGPT Agent',
|
||||||
description: 'A ChatGPT-powered assistant for clear, useful answers.',
|
title: {
|
||||||
|
default: title,
|
||||||
|
template: '%s | ChatGPT Agent',
|
||||||
|
},
|
||||||
|
description,
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
siteName: 'ChatGPT Agent',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
+7
-1
@@ -1,5 +1,11 @@
|
|||||||
import { ChatAgent } from "@/components/chat/ChatAgent";
|
import { ChatAgent } from "@/components/chat/ChatAgent";
|
||||||
|
import { getDefaultChatModelId } from "@/services/chat/model";
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
return <ChatAgent webSearchEnabled={Boolean(process.env.FIRECRAWL_API_KEY)} />;
|
return (
|
||||||
|
<ChatAgent
|
||||||
|
webSearchEnabled={Boolean(process.env.FIRECRAWL_API_KEY)}
|
||||||
|
defaultModelId={getDefaultChatModelId()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { firecrawlWebFetch } from '@/actions/tools/firecrawl-web-fetch';
|
|
||||||
import { FirecrawlWebFetchTool } from '@/components/tools/firecrawl-web-fetch-tool';
|
|
||||||
import { firecrawlWebSearch } from '@/actions/tools/firecrawl-web-search';
|
|
||||||
import { FirecrawlWebSearchTool } from '@/components/tools/firecrawl-web-search-tool';
|
|
||||||
|
|
||||||
export const tools = {
|
|
||||||
firecrawlWebSearch,
|
|
||||||
firecrawlWebFetch,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toolComponents = {
|
|
||||||
firecrawlWebSearch: FirecrawlWebSearchTool,
|
|
||||||
firecrawlWebFetch: FirecrawlWebFetchTool,
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { type LucideIcon, XIcon } from "lucide-react";
|
import { type RemixiconComponentType, RiCloseLine } from "@remixicon/react";
|
||||||
import type { ComponentProps, HTMLAttributes } from "react";
|
import type { ComponentProps, HTMLAttributes } from "react";
|
||||||
|
|
||||||
export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
|
export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
|
||||||
@@ -57,7 +57,7 @@ export const ArtifactClose = ({
|
|||||||
variant={variant}
|
variant={variant}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <XIcon className="size-4" />}
|
{children ?? <RiCloseLine className="size-4" />}
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -92,7 +92,7 @@ export const ArtifactActions = ({
|
|||||||
export type ArtifactActionProps = ComponentProps<typeof Button> & {
|
export type ArtifactActionProps = ComponentProps<typeof Button> & {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
icon?: LucideIcon;
|
icon?: RemixiconComponentType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ArtifactAction = ({
|
export const ArtifactAction = ({
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import {
|
|||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
BrainIcon,
|
RiBrainLine,
|
||||||
ChevronDownIcon,
|
RiArrowDownSLine,
|
||||||
DotIcon,
|
RiCircleFill,
|
||||||
type LucideIcon,
|
type RemixiconComponentType,
|
||||||
} from "lucide-react";
|
} from "@remixicon/react";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import { createContext, memo, useContext, useMemo } from "react";
|
import { createContext, memo, useContext, useMemo } from "react";
|
||||||
|
|
||||||
@@ -92,11 +92,11 @@ export const ChainOfThoughtHeader = memo(
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<BrainIcon className="size-4" />
|
<RiBrainLine className="size-4" />
|
||||||
<span className="flex-1 text-left">
|
<span className="flex-1 text-left">
|
||||||
{children ?? "Chain of Thought"}
|
{children ?? "Chain of Thought"}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDownIcon
|
<RiArrowDownSLine
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-4 transition-transform",
|
"size-4 transition-transform",
|
||||||
isOpen ? "rotate-180" : "rotate-0"
|
isOpen ? "rotate-180" : "rotate-0"
|
||||||
@@ -109,7 +109,7 @@ export const ChainOfThoughtHeader = memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
|
export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
|
||||||
icon?: LucideIcon;
|
icon?: RemixiconComponentType;
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
status?: "complete" | "active" | "pending";
|
status?: "complete" | "active" | "pending";
|
||||||
@@ -118,7 +118,7 @@ export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
|
|||||||
export const ChainOfThoughtStep = memo(
|
export const ChainOfThoughtStep = memo(
|
||||||
({
|
({
|
||||||
className,
|
className,
|
||||||
icon: Icon = DotIcon,
|
icon: Icon = RiCircleFill,
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
status = "complete",
|
status = "complete",
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { BookmarkIcon, type LucideProps } from "lucide-react";
|
import { RiBookmarkLine } from "@remixicon/react";
|
||||||
import type { ComponentProps, HTMLAttributes } from "react";
|
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
|
||||||
|
|
||||||
export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
|
export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
@@ -27,7 +27,9 @@ export const Checkpoint = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type CheckpointIconProps = LucideProps;
|
export type CheckpointIconProps = Omit<ComponentProps<typeof RiBookmarkLine>, "children"> & {
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
export const CheckpointIcon = ({
|
export const CheckpointIcon = ({
|
||||||
className,
|
className,
|
||||||
@@ -35,7 +37,7 @@ export const CheckpointIcon = ({
|
|||||||
...props
|
...props
|
||||||
}: CheckpointIconProps) =>
|
}: CheckpointIconProps) =>
|
||||||
children ?? (
|
children ?? (
|
||||||
<BookmarkIcon className={cn("size-4 shrink-0", className)} {...props} />
|
<RiBookmarkLine className={cn("size-4 shrink-0", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
|
export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
import { RiCheckLine, RiFileCopyLine } from "@remixicon/react";
|
||||||
import {
|
import {
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
createContext,
|
createContext,
|
||||||
@@ -162,7 +162,7 @@ export const CodeBlockCopyButton = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Icon = isCopied ? CheckIcon : CopyIcon;
|
const Icon = isCopied ? RiCheckLine : RiFileCopyLine;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ArrowDownIcon } from "lucide-react";
|
import { RiArrowDownLine } from "@remixicon/react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
||||||
@@ -93,7 +93,7 @@ export const ConversationScrollButton = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowDownIcon className="size-4" />
|
<RiArrowDownLine className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from "@/components/ui/hover-card";
|
} from "@/components/ui/hover-card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
import { RiArrowLeftLine, RiArrowRightLine } from "@remixicon/react";
|
||||||
import {
|
import {
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
createContext,
|
createContext,
|
||||||
@@ -206,7 +206,7 @@ export const InlineCitationCarouselPrev = ({
|
|||||||
type="button"
|
type="button"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="size-4 text-muted-foreground" />
|
<RiArrowLeftLine className="size-4 text-muted-foreground" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -233,7 +233,7 @@ export const InlineCitationCarouselNext = ({
|
|||||||
type="button"
|
type="button"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowRightIcon className="size-4 text-muted-foreground" />
|
<RiArrowRightLine className="size-4 text-muted-foreground" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { FileUIPart, UIMessage } from "ai";
|
import type { FileUIPart, UIMessage } from "ai";
|
||||||
import {
|
import {
|
||||||
ChevronLeftIcon,
|
RiArrowLeftSLine,
|
||||||
ChevronRightIcon,
|
RiArrowRightSLine,
|
||||||
PaperclipIcon,
|
RiAttachment2,
|
||||||
XIcon,
|
RiCloseLine,
|
||||||
} from "lucide-react";
|
} from "@remixicon/react";
|
||||||
import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode } from "react";
|
import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
Children,
|
Children,
|
||||||
@@ -267,7 +267,7 @@ export const MessageBranchPrevious = ({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <ChevronLeftIcon size={14} />}
|
{children ?? <RiArrowLeftSLine size={14} />}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -291,7 +291,7 @@ export const MessageBranchNext = ({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <ChevronRightIcon size={14} />}
|
{children ?? <RiArrowRightSLine size={14} />}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -338,8 +338,7 @@ export const MessageResponse = memo(
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
(prevProps, nextProps) => prevProps.children === nextProps.children
|
|
||||||
);
|
);
|
||||||
|
|
||||||
MessageResponse.displayName = "MessageResponse";
|
MessageResponse.displayName = "MessageResponse";
|
||||||
@@ -390,7 +389,7 @@ export function MessageAttachment({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<RiCloseLine />
|
||||||
<span className="sr-only">Remove</span>
|
<span className="sr-only">Remove</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -400,7 +399,7 @@ export function MessageAttachment({
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex size-full shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
<div className="flex size-full shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
||||||
<PaperclipIcon className="size-4" />
|
<RiAttachment2 className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -418,7 +417,7 @@ export function MessageAttachment({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<RiCloseLine />
|
||||||
<span className="sr-only">Remove</span>
|
<span className="sr-only">Remove</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
RiArrowDownSLine,
|
||||||
ExternalLinkIcon,
|
RiExternalLinkLine,
|
||||||
MessageCircleIcon,
|
RiChat3Line,
|
||||||
} from "lucide-react";
|
} from "@remixicon/react";
|
||||||
import { type ComponentProps, createContext, useContext } from "react";
|
import { type ComponentProps, createContext, useContext } from "react";
|
||||||
|
|
||||||
const providers = {
|
const providers = {
|
||||||
@@ -140,7 +140,7 @@ const providers = {
|
|||||||
`https://t3.chat/new?${new URLSearchParams({
|
`https://t3.chat/new?${new URLSearchParams({
|
||||||
q,
|
q,
|
||||||
})}`,
|
})}`,
|
||||||
icon: <MessageCircleIcon />,
|
icon: <RiChat3Line />,
|
||||||
},
|
},
|
||||||
v0: {
|
v0: {
|
||||||
title: "Open in v0",
|
title: "Open in v0",
|
||||||
@@ -238,7 +238,7 @@ export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => (
|
|||||||
{children ?? (
|
{children ?? (
|
||||||
<Button type="button" variant="outline">
|
<Button type="button" variant="outline">
|
||||||
Open in chat
|
Open in chat
|
||||||
<ChevronDownIcon className="size-4" />
|
<RiArrowDownSLine className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -258,7 +258,7 @@ export const OpenInChatGPT = (props: OpenInChatGPTProps) => {
|
|||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.chatgpt.icon}</span>
|
<span className="shrink-0">{providers.chatgpt.icon}</span>
|
||||||
<span className="flex-1">{providers.chatgpt.title}</span>
|
<span className="flex-1">{providers.chatgpt.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<RiExternalLinkLine className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
@@ -278,7 +278,7 @@ export const OpenInClaude = (props: OpenInClaudeProps) => {
|
|||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.claude.icon}</span>
|
<span className="shrink-0">{providers.claude.icon}</span>
|
||||||
<span className="flex-1">{providers.claude.title}</span>
|
<span className="flex-1">{providers.claude.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<RiExternalLinkLine className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
@@ -298,7 +298,7 @@ export const OpenInT3 = (props: OpenInT3Props) => {
|
|||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.t3.icon}</span>
|
<span className="shrink-0">{providers.t3.icon}</span>
|
||||||
<span className="flex-1">{providers.t3.title}</span>
|
<span className="flex-1">{providers.t3.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<RiExternalLinkLine className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
@@ -318,7 +318,7 @@ export const OpenInScira = (props: OpenInSciraProps) => {
|
|||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.scira.icon}</span>
|
<span className="shrink-0">{providers.scira.icon}</span>
|
||||||
<span className="flex-1">{providers.scira.title}</span>
|
<span className="flex-1">{providers.scira.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<RiExternalLinkLine className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
@@ -338,7 +338,7 @@ export const OpenInv0 = (props: OpenInv0Props) => {
|
|||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.v0.icon}</span>
|
<span className="shrink-0">{providers.v0.icon}</span>
|
||||||
<span className="flex-1">{providers.v0.title}</span>
|
<span className="flex-1">{providers.v0.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<RiExternalLinkLine className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
@@ -358,7 +358,7 @@ export const OpenInCursor = (props: OpenInCursorProps) => {
|
|||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.cursor.icon}</span>
|
<span className="shrink-0">{providers.cursor.icon}</span>
|
||||||
<span className="flex-1">{providers.cursor.title}</span>
|
<span className="flex-1">{providers.cursor.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<RiExternalLinkLine className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ChevronsUpDownIcon } from "lucide-react";
|
import { RiExpandUpDownLine } from "@remixicon/react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
import { Shimmer } from "./shimmer";
|
import { Shimmer } from "./shimmer";
|
||||||
@@ -135,7 +135,7 @@ export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => (
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronsUpDownIcon className="size-4" />
|
<RiExpandUpDownLine className="size-4" />
|
||||||
<span className="sr-only">Toggle plan</span>
|
<span className="sr-only">Toggle plan</span>
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
|
|||||||
@@ -37,14 +37,14 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ChatStatus, FileUIPart } from "ai";
|
import type { ChatStatus, FileUIPart } from "ai";
|
||||||
import {
|
import {
|
||||||
CornerDownLeftIcon,
|
RiCornerDownLeftLine,
|
||||||
ImageIcon,
|
RiImageLine,
|
||||||
MicIcon,
|
RiMicLine,
|
||||||
PaperclipIcon,
|
RiAttachment2,
|
||||||
PlusIcon,
|
RiAddLine,
|
||||||
SquareIcon,
|
RiStopFill,
|
||||||
XIcon,
|
RiCloseLine,
|
||||||
} from "lucide-react";
|
} from "@remixicon/react";
|
||||||
import { Loader } from "@/components/ai-elements/loader";
|
import { Loader } from "@/components/ai-elements/loader";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import {
|
import {
|
||||||
@@ -319,7 +319,7 @@ export function PromptInputAttachment({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex size-5 items-center justify-center text-muted-foreground">
|
<div className="flex size-5 items-center justify-center text-muted-foreground">
|
||||||
<PaperclipIcon className="size-3" />
|
<RiAttachment2 className="size-3" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -333,7 +333,7 @@ export function PromptInputAttachment({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<RiCloseLine />
|
||||||
<span className="sr-only">Remove</span>
|
<span className="sr-only">Remove</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -421,7 +421,7 @@ export const PromptInputActionAddAttachments = ({
|
|||||||
attachments.openFileDialog();
|
attachments.openFileDialog();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ImageIcon className="mr-2 size-4" /> {label}
|
<RiImageLine className="mr-2 size-4" /> {label}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -989,7 +989,7 @@ export const PromptInputActionMenuTrigger = ({
|
|||||||
}: PromptInputActionMenuTriggerProps) => (
|
}: PromptInputActionMenuTriggerProps) => (
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<PromptInputButton className={className} {...props}>
|
<PromptInputButton className={className} {...props}>
|
||||||
{children ?? <PlusIcon className="size-4" />}
|
{children ?? <RiAddLine className="size-4" />}
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
);
|
);
|
||||||
@@ -1029,14 +1029,14 @@ export const PromptInputSubmit = ({
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: PromptInputSubmitProps) => {
|
}: PromptInputSubmitProps) => {
|
||||||
let Icon = <CornerDownLeftIcon className="size-4" />;
|
let Icon = <RiCornerDownLeftLine className="size-4" />;
|
||||||
|
|
||||||
if (status === "submitted") {
|
if (status === "submitted") {
|
||||||
Icon = <Loader size={16} />;
|
Icon = <Loader size={16} />;
|
||||||
} else if (status === "streaming") {
|
} else if (status === "streaming") {
|
||||||
Icon = <SquareIcon className="size-4" />;
|
Icon = <RiStopFill className="size-4" />;
|
||||||
} else if (status === "error") {
|
} else if (status === "error") {
|
||||||
Icon = <XIcon className="size-4" />;
|
Icon = <RiCloseLine className="size-4" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1208,7 +1208,7 @@ export const PromptInputSpeechButton = ({
|
|||||||
onClick={toggleListening}
|
onClick={toggleListening}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MicIcon className="size-4" />
|
<RiMicLine className="size-4" />
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ChevronDownIcon, PaperclipIcon } from "lucide-react";
|
import { RiArrowDownSLine, RiAttachment2 } from "@remixicon/react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
export type QueueMessagePart = {
|
export type QueueMessagePart = {
|
||||||
@@ -174,7 +174,7 @@ export const QueueItemFile = ({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<PaperclipIcon size={12} />
|
<RiAttachment2 size={12} />
|
||||||
<span className="max-w-[100px] truncate">{children}</span>
|
<span className="max-w-[100px] truncate">{children}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -241,7 +241,7 @@ export const QueueSectionLabel = ({
|
|||||||
...props
|
...props
|
||||||
}: QueueSectionLabelProps) => (
|
}: QueueSectionLabelProps) => (
|
||||||
<span className={cn("flex items-center gap-2", className)} {...props}>
|
<span className={cn("flex items-center gap-2", className)} {...props}>
|
||||||
<ChevronDownIcon className="group-data-[state=closed]:-rotate-90 size-4 transition-transform" />
|
<RiArrowDownSLine className="group-data-[state=closed]:-rotate-90 size-4 transition-transform" />
|
||||||
{icon}
|
{icon}
|
||||||
<span>
|
<span>
|
||||||
{count} {label}
|
{count} {label}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
import { RiBrainLine, RiArrowDownSLine } from "@remixicon/react";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
import { createContext, memo, useContext, useEffect, useState } from "react";
|
||||||
import { Streamdown } from "streamdown";
|
import { Streamdown } from "streamdown";
|
||||||
@@ -139,9 +139,9 @@ export const ReasoningTrigger = memo(
|
|||||||
>
|
>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<>
|
||||||
<BrainIcon className="size-4" />
|
<RiBrainLine className="size-4" />
|
||||||
{getThinkingMessage(isStreaming, duration)}
|
{getThinkingMessage(isStreaming, duration)}
|
||||||
<ChevronDownIcon
|
<RiArrowDownSLine
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-4 transition-transform",
|
"size-4 transition-transform",
|
||||||
isOpen ? "rotate-180" : "rotate-0"
|
isOpen ? "rotate-180" : "rotate-0"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@/components/ui/tabs";
|
} from "@/components/ui/tabs";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ChevronDownIcon, Code } from "lucide-react";
|
import { RiArrowDownSLine, RiCodeLine } from "@remixicon/react";
|
||||||
|
|
||||||
import { getStatusBadge } from "./tool";
|
import { getStatusBadge } from "./tool";
|
||||||
|
|
||||||
@@ -52,11 +52,11 @@ export const SandboxHeader = ({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Code className="size-4 text-muted-foreground" />
|
<RiCodeLine className="size-4 text-muted-foreground" />
|
||||||
<span className="font-medium text-sm">{title}</span>
|
<span className="font-medium text-sm">{title}</span>
|
||||||
{getStatusBadge(state)}
|
{getStatusBadge(state)}
|
||||||
</div>
|
</div>
|
||||||
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
<RiArrowDownSLine className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { BookIcon, ChevronDownIcon } from "lucide-react";
|
import { RiBookOpenLine, RiArrowDownSLine } from "@remixicon/react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
export type SourcesProps = ComponentProps<"div">;
|
export type SourcesProps = ComponentProps<"div">;
|
||||||
@@ -35,7 +35,7 @@ export const SourcesTrigger = ({
|
|||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<>
|
||||||
<p className="font-medium">Used {count} sources</p>
|
<p className="font-medium">Used {count} sources</p>
|
||||||
<ChevronDownIcon className="h-4 w-4" />
|
<RiArrowDownSLine className="h-4 w-4" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
@@ -69,7 +69,7 @@ export const Source = ({ href, title, children, ...props }: SourceProps) => (
|
|||||||
>
|
>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<>
|
||||||
<BookIcon className="h-4 w-4" />
|
<RiBookOpenLine className="h-4 w-4" />
|
||||||
<span className="block font-medium">{title}</span>
|
<span className="block font-medium">{title}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ChevronDownIcon, SearchIcon } from "lucide-react";
|
import { RiArrowDownSLine, RiSearchLine } from "@remixicon/react";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
|
|
||||||
export type TaskItemFileProps = ComponentProps<"div">;
|
export type TaskItemFileProps = ComponentProps<"div">;
|
||||||
@@ -60,9 +60,9 @@ export const TaskTrigger = ({
|
|||||||
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
|
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<div className="flex w-full cursor-pointer items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground">
|
<div className="flex w-full cursor-pointer items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground">
|
||||||
{icon ?? <SearchIcon className="size-4" />}
|
{icon ?? <RiSearchLine className="size-4" />}
|
||||||
<p className="text-sm">{title}</p>
|
<p className="text-sm">{title}</p>
|
||||||
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
<RiArrowDownSLine className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ToolUIPart } from "ai";
|
import type { ToolUIPart } from "ai";
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
RiCheckboxCircleLine,
|
||||||
ChevronDownIcon,
|
RiArrowDownSLine,
|
||||||
CircleIcon,
|
RiCircleLine,
|
||||||
ClockIcon,
|
RiTimeLine,
|
||||||
WrenchIcon,
|
RiToolsLine,
|
||||||
XCircleIcon,
|
RiCloseCircleLine,
|
||||||
} from "lucide-react";
|
} from "@remixicon/react";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import { isValidElement } from "react";
|
import { isValidElement } from "react";
|
||||||
import { CodeBlock } from "./code-block";
|
import { CodeBlock } from "./code-block";
|
||||||
@@ -48,13 +48,13 @@ export const getStatusBadge = (status: ToolUIPart["state"]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const icons: Record<ToolUIPart["state"], ReactNode> = {
|
const icons: Record<ToolUIPart["state"], ReactNode> = {
|
||||||
"input-streaming": <CircleIcon className="size-4" />,
|
"input-streaming": <RiCircleLine className="size-4" />,
|
||||||
"input-available": <ClockIcon className="size-4 animate-pulse" />,
|
"input-available": <RiTimeLine className="size-4 animate-pulse" />,
|
||||||
"approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
|
"approval-requested": <RiTimeLine className="size-4 text-yellow-600" />,
|
||||||
"approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
|
"approval-responded": <RiCheckboxCircleLine className="size-4 text-blue-600" />,
|
||||||
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
|
"output-available": <RiCheckboxCircleLine className="size-4 text-green-600" />,
|
||||||
"output-error": <XCircleIcon className="size-4 text-red-600" />,
|
"output-error": <RiCloseCircleLine className="size-4 text-red-600" />,
|
||||||
"output-denied": <XCircleIcon className="size-4 text-orange-600" />,
|
"output-denied": <RiCloseCircleLine className="size-4 text-orange-600" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,13 +80,13 @@ export const ToolHeader = ({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<WrenchIcon className="size-4 text-muted-foreground" />
|
<RiToolsLine className="size-4 text-muted-foreground" />
|
||||||
<span className="font-medium text-sm">
|
<span className="font-medium text-sm">
|
||||||
{title ?? type.split("-").slice(1).join("-")}
|
{title ?? type.split("-").slice(1).join("-")}
|
||||||
</span>
|
</span>
|
||||||
{getStatusBadge(state)}
|
{getStatusBadge(state)}
|
||||||
</div>
|
</div>
|
||||||
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
<RiArrowDownSLine className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ChevronDownIcon } from "lucide-react";
|
import { RiArrowDownSLine } from "@remixicon/react";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ export const WebPreviewConsole = ({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
Console
|
Console
|
||||||
<ChevronDownIcon
|
<RiArrowDownSLine
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4 transition-transform duration-200",
|
"h-4 w-4 transition-transform duration-200",
|
||||||
consoleOpen && "rotate-180"
|
consoleOpen && "rotate-180"
|
||||||
|
|||||||
+37
-230
@@ -1,20 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
import { ChatInput } from './ChatInput';
|
||||||
import type { ChatStatus } from 'ai';
|
|
||||||
import { ChatInput, type PromptInputMessage } from './ChatInput';
|
|
||||||
import { ChatMessages } from './ChatMessages';
|
import { ChatMessages } from './ChatMessages';
|
||||||
import { ChatPrompt, PromptSuggestion } from './ChatPrompt';
|
import { ChatPrompt } from './ChatPrompt';
|
||||||
import { ChatSidebar, type Chat } from './ChatSidebar';
|
import { ChatSidebar } from './ChatSidebar';
|
||||||
import { useChat, UIMessage } from '@ai-sdk/react';
|
import { useChatSession } from '@/hooks/use-chat-session';
|
||||||
import { DefaultChatTransport } from 'ai';
|
import type { PromptSuggestion } from '@/config/suggestions';
|
||||||
import {
|
import { DEFAULT_OPENROUTER_MODEL_ID, type OpenRouterModelId } from '@/lib/openrouter';
|
||||||
createChat,
|
|
||||||
deleteChat,
|
|
||||||
getMessages,
|
|
||||||
listChats,
|
|
||||||
saveMessages,
|
|
||||||
} from '@/services/indexed-db/chat-store';
|
|
||||||
import {
|
import {
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
@@ -24,228 +16,32 @@ import {
|
|||||||
interface ChatAgentProps {
|
interface ChatAgentProps {
|
||||||
suggestions?: PromptSuggestion[];
|
suggestions?: PromptSuggestion[];
|
||||||
webSearchEnabled?: boolean;
|
webSearchEnabled?: boolean;
|
||||||
}
|
defaultModelId?: OpenRouterModelId;
|
||||||
|
|
||||||
async function convertToDataUrl(url: string): Promise<string> {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) throw new Error('Could not read the selected file');
|
|
||||||
|
|
||||||
const blob = await response.blob();
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
if (typeof reader.result === 'string') {
|
|
||||||
resolve(reader.result);
|
|
||||||
} else {
|
|
||||||
reject(new Error('Could not encode the selected file'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.onerror = () => reject(reader.error);
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatAgent({
|
export function ChatAgent({
|
||||||
suggestions,
|
suggestions,
|
||||||
webSearchEnabled = false,
|
webSearchEnabled = false,
|
||||||
|
defaultModelId = DEFAULT_OPENROUTER_MODEL_ID,
|
||||||
}: ChatAgentProps) {
|
}: ChatAgentProps) {
|
||||||
const [inputValue, setInputValue] = useState('');
|
|
||||||
const [chatId, setChatId] = useState<string | null>(null);
|
|
||||||
const [chats, setChats] = useState<Chat[]>([]);
|
|
||||||
const chatIdRef = useRef(chatId);
|
|
||||||
const pendingChatIdRef = useRef<string | null>(null);
|
|
||||||
chatIdRef.current = chatId;
|
|
||||||
|
|
||||||
const fetchChats = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setChats(await listChats());
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch chats:', error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const transportRef = useRef(
|
|
||||||
new DefaultChatTransport({
|
|
||||||
api: '/api/chat',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
inputValue,
|
||||||
|
setInputValue,
|
||||||
|
modelId,
|
||||||
|
setModelId,
|
||||||
|
chatId,
|
||||||
|
chats,
|
||||||
|
chatMessages,
|
||||||
status,
|
status,
|
||||||
messages: chatMessages,
|
chatStatus,
|
||||||
setMessages: setChatMessages,
|
aiError,
|
||||||
sendMessage,
|
isPreparingFiles,
|
||||||
stop,
|
handleSelectChat,
|
||||||
error: aiError,
|
handleNewChat,
|
||||||
addToolApprovalResponse,
|
handleDeleteChat,
|
||||||
} = useChat({
|
handleSendMessage,
|
||||||
transport: transportRef.current,
|
handleStop,
|
||||||
onFinish: async ({ messages }) => {
|
} = useChatSession(defaultModelId);
|
||||||
const activeChatId = pendingChatIdRef.current ?? chatIdRef.current;
|
|
||||||
if (!activeChatId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await saveMessages(activeChatId, messages);
|
|
||||||
await fetchChats();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save messages:', error);
|
|
||||||
} finally {
|
|
||||||
if (pendingChatIdRef.current === activeChatId) {
|
|
||||||
pendingChatIdRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Chat error:', error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isPreparingFiles, setIsPreparingFiles] = useState(false);
|
|
||||||
|
|
||||||
const chatStatus: ChatStatus = isPreparingFiles
|
|
||||||
? 'submitted'
|
|
||||||
: status === 'submitted' || status === 'streaming'
|
|
||||||
? status
|
|
||||||
: 'ready';
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchChats();
|
|
||||||
}, [fetchChats]);
|
|
||||||
|
|
||||||
const handleSelectChat = useCallback(
|
|
||||||
async (selectedChatId: string) => {
|
|
||||||
try {
|
|
||||||
const messages = await getMessages(selectedChatId);
|
|
||||||
chatIdRef.current = selectedChatId;
|
|
||||||
setChatId(selectedChatId);
|
|
||||||
|
|
||||||
setChatMessages(messages);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load chat:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setChatMessages]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleNewChat = useCallback(() => {
|
|
||||||
chatIdRef.current = null;
|
|
||||||
setChatId(null);
|
|
||||||
setChatMessages([]);
|
|
||||||
setInputValue('');
|
|
||||||
}, [setChatMessages]);
|
|
||||||
|
|
||||||
const handleDeleteChat = useCallback(
|
|
||||||
async (deletedChatId: string) => {
|
|
||||||
try {
|
|
||||||
await deleteChat(deletedChatId);
|
|
||||||
setChats((prev) => prev.filter((c) => c.id !== deletedChatId));
|
|
||||||
if (chatId === deletedChatId) {
|
|
||||||
chatIdRef.current = null;
|
|
||||||
setChatId(null);
|
|
||||||
setChatMessages([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete chat:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[chatId, setChatMessages]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSendMessage = useCallback(
|
|
||||||
async (message: PromptInputMessage) => {
|
|
||||||
const trimmedValue = message.text?.trim();
|
|
||||||
if (!trimmedValue && !message.files?.length) return;
|
|
||||||
if (
|
|
||||||
status === 'submitted' ||
|
|
||||||
status === 'streaming' ||
|
|
||||||
isPreparingFiles
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
let activeChatId = chatIdRef.current;
|
|
||||||
|
|
||||||
if (!chatIdRef.current) {
|
|
||||||
const newChatId = crypto.randomUUID();
|
|
||||||
const title = trimmedValue ? trimmedValue.slice(0, 100) : 'New Chat';
|
|
||||||
|
|
||||||
activeChatId = newChatId;
|
|
||||||
chatIdRef.current = newChatId;
|
|
||||||
setChatId(newChatId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createChat(newChatId, title);
|
|
||||||
await fetchChats();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to create chat:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts: Array<
|
|
||||||
| { type: 'text'; text: string }
|
|
||||||
| { type: 'file'; url: string; mediaType: string; filename: string }
|
|
||||||
> = [];
|
|
||||||
|
|
||||||
if (trimmedValue) {
|
|
||||||
parts.push({ type: 'text', text: trimmedValue });
|
|
||||||
}
|
|
||||||
|
|
||||||
// PromptInput supplies files as base64 data URLs. Keep them inline so the
|
|
||||||
// demo never needs a remote storage bucket.
|
|
||||||
if (message.files?.length) {
|
|
||||||
setIsPreparingFiles(true);
|
|
||||||
for (const file of message.files) {
|
|
||||||
try {
|
|
||||||
const dataUrl = file.url.startsWith('data:')
|
|
||||||
? file.url
|
|
||||||
: await convertToDataUrl(file.url);
|
|
||||||
|
|
||||||
parts.push({
|
|
||||||
type: 'file',
|
|
||||||
url: dataUrl,
|
|
||||||
mediaType: file.mediaType || 'application/octet-stream',
|
|
||||||
filename: file.filename || 'file',
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('File encoding error:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setIsPreparingFiles(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length === 0) return;
|
|
||||||
|
|
||||||
const userMessage: UIMessage = {
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
role: 'user',
|
|
||||||
parts,
|
|
||||||
};
|
|
||||||
|
|
||||||
setInputValue('');
|
|
||||||
|
|
||||||
if (activeChatId) {
|
|
||||||
try {
|
|
||||||
await saveMessages(activeChatId, [...chatMessages, userMessage]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save user message:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingChatIdRef.current = activeChatId;
|
|
||||||
await sendMessage(userMessage);
|
|
||||||
},
|
|
||||||
[
|
|
||||||
status,
|
|
||||||
sendMessage,
|
|
||||||
chatMessages,
|
|
||||||
isPreparingFiles,
|
|
||||||
fetchChats,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleStop = useCallback(() => {
|
|
||||||
stop();
|
|
||||||
}, [stop]);
|
|
||||||
|
|
||||||
const hasMessages = chatMessages.length > 0;
|
const hasMessages = chatMessages.length > 0;
|
||||||
|
|
||||||
@@ -268,6 +64,9 @@ export function ChatAgent({
|
|||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={setInputValue}
|
onChange={setInputValue}
|
||||||
onSubmit={handleSendMessage}
|
onSubmit={handleSendMessage}
|
||||||
|
modelId={modelId}
|
||||||
|
onModelChange={setModelId}
|
||||||
|
onStop={isPreparingFiles ? undefined : handleStop}
|
||||||
status={chatStatus}
|
status={chatStatus}
|
||||||
suggestions={suggestions}
|
suggestions={suggestions}
|
||||||
webSearchEnabled={webSearchEnabled}
|
webSearchEnabled={webSearchEnabled}
|
||||||
@@ -281,15 +80,23 @@ export function ChatAgent({
|
|||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<ChatMessages
|
<ChatMessages
|
||||||
messages={chatMessages}
|
messages={chatMessages}
|
||||||
isLoading={status === 'submitted' || status === 'streaming'}
|
status={status}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky bottom-0 bg-muted p-4">
|
<div className="sticky bottom-0 bg-muted p-4">
|
||||||
<div className="mx-auto max-w-3xl">
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{aiError && (
|
||||||
|
<p role="alert" className="mb-3 text-sm text-destructive">
|
||||||
|
The response could not be completed. Please try again.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<ChatInput
|
<ChatInput
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={setInputValue}
|
onChange={setInputValue}
|
||||||
onSubmit={handleSendMessage}
|
onSubmit={handleSendMessage}
|
||||||
|
modelId={modelId}
|
||||||
|
onModelChange={setModelId}
|
||||||
|
onStop={isPreparingFiles ? undefined : handleStop}
|
||||||
status={chatStatus}
|
status={chatStatus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,4 +108,4 @@ export function ChatAgent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { UIMessage as Message };
|
export type { UIMessage as Message } from 'ai';
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ChatStatus } from 'ai';
|
import type { ChatStatus } from 'ai';
|
||||||
import { PlusIcon } from 'lucide-react';
|
import { RiAddLine } from '@remixicon/react';
|
||||||
|
import { ChatModelSelector } from './ChatModelSelector';
|
||||||
|
import type { OpenRouterModelId } from '@/lib/openrouter';
|
||||||
import {
|
import {
|
||||||
PromptInput,
|
PromptInput,
|
||||||
PromptInputActionAddAttachments,
|
PromptInputActionAddAttachments,
|
||||||
@@ -23,6 +25,9 @@ interface ChatInputProps {
|
|||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onSubmit: (message: PromptInputMessage) => void;
|
onSubmit: (message: PromptInputMessage) => void;
|
||||||
|
modelId: OpenRouterModelId;
|
||||||
|
onModelChange: (modelId: OpenRouterModelId) => void;
|
||||||
|
onStop?: () => void;
|
||||||
status?: ChatStatus;
|
status?: ChatStatus;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
@@ -31,9 +36,14 @@ export function ChatInput({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
modelId,
|
||||||
|
onModelChange,
|
||||||
|
onStop,
|
||||||
status = 'ready',
|
status = 'ready',
|
||||||
placeholder,
|
placeholder,
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
|
const isGenerating = status === 'submitted' || status === 'streaming';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PromptInput
|
<PromptInput
|
||||||
className="[&_[data-slot=input-group]]:bg-background"
|
className="[&_[data-slot=input-group]]:bg-background"
|
||||||
@@ -55,18 +65,22 @@ export function ChatInput({
|
|||||||
/>
|
/>
|
||||||
</PromptInputBody>
|
</PromptInputBody>
|
||||||
<PromptInputFooter className="pt-0 pb-2">
|
<PromptInputFooter className="pt-0 pb-2">
|
||||||
<PromptInputTools>
|
<PromptInputTools className="min-w-0">
|
||||||
<PromptInputActionMenu>
|
<PromptInputActionMenu>
|
||||||
<PromptInputActionMenuTrigger>
|
<PromptInputActionMenuTrigger>
|
||||||
<PlusIcon className="size-4" />
|
<RiAddLine className="size-4" />
|
||||||
</PromptInputActionMenuTrigger>
|
</PromptInputActionMenuTrigger>
|
||||||
<PromptInputActionMenuContent>
|
<PromptInputActionMenuContent>
|
||||||
<PromptInputActionAddAttachments />
|
<PromptInputActionAddAttachments />
|
||||||
</PromptInputActionMenuContent>
|
</PromptInputActionMenuContent>
|
||||||
</PromptInputActionMenu>
|
</PromptInputActionMenu>
|
||||||
|
<ChatModelSelector value={modelId} onChange={onModelChange} disabled={isGenerating} />
|
||||||
</PromptInputTools>
|
</PromptInputTools>
|
||||||
<PromptInputSubmit
|
<PromptInputSubmit
|
||||||
disabled={status === 'streaming'}
|
aria-label={isGenerating ? 'Stop response' : 'Send message'}
|
||||||
|
type={isGenerating ? 'button' : 'submit'}
|
||||||
|
onClick={isGenerating ? onStop : undefined}
|
||||||
|
disabled={isGenerating && !onStop}
|
||||||
status={status}
|
status={status}
|
||||||
/>
|
/>
|
||||||
</PromptInputFooter>
|
</PromptInputFooter>
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Tool, UIMessage, UIMessagePart } from "ai";
|
|
||||||
import { MessagePartRenderer } from "./ChatMessageParts";
|
|
||||||
|
|
||||||
interface ChatMessageProps {
|
|
||||||
message: UIMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTextContent(parts: UIMessagePart<any, any>[]): string {
|
|
||||||
return parts
|
|
||||||
.filter((p) => p.type === "text")
|
|
||||||
.map((p) => p.text || "")
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChatMessage({ message }: ChatMessageProps) {
|
|
||||||
const isUser = message.role === "user";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex w-full",
|
|
||||||
isUser ? "justify-end" : "justify-start"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"max-w-[80%] rounded-2xl px-4 py-3",
|
|
||||||
isUser
|
|
||||||
? "bg-primary text-primary-foreground"
|
|
||||||
: "bg-muted text-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"prose prose-sm max-w-none",
|
|
||||||
isUser
|
|
||||||
? "prose-invert prose-p:text-primary-foreground prose-headings:text-primary-foreground prose-strong:text-primary-foreground prose-code:text-primary-foreground"
|
|
||||||
: "prose-p:text-foreground prose-headings:text-foreground prose-strong:text-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{message.parts.map((part, index) => (
|
|
||||||
<MessagePartRenderer key={index} part={part} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getTextContent };
|
|
||||||
export type { UIMessage as Message };
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { UIMessage } from "ai";
|
|
||||||
import {
|
|
||||||
Tool,
|
|
||||||
ToolContent,
|
|
||||||
ToolHeader,
|
|
||||||
ToolInput,
|
|
||||||
ToolOutput,
|
|
||||||
} from "@/components/ai-elements/tool";
|
|
||||||
import { Loader } from "@/components/ai-elements/loader";
|
|
||||||
import { toolComponents } from "@/chat.config";
|
|
||||||
|
|
||||||
type MessagePart = UIMessage["parts"][number];
|
|
||||||
type ReasoningPart = Extract<MessagePart, { type: "reasoning" }>;
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
export function getTextContent(parts: MessagePart[]): string {
|
|
||||||
return parts
|
|
||||||
.filter((p) => p.type === "text")
|
|
||||||
.map((p) => p.text || "")
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getReasoningPart(parts: MessagePart[]): ReasoningPart | undefined {
|
|
||||||
return parts.find((p): p is ReasoningPart => p.type === "reasoning");
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractToolName(part: MessagePart): string {
|
|
||||||
if (part.type === "dynamic-tool") {
|
|
||||||
return (part as any).toolName || "unknown";
|
|
||||||
}
|
|
||||||
if (part.type.startsWith("tool-")) {
|
|
||||||
return part.type.slice(5);
|
|
||||||
}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
function DefaultToolRenderer({ part, toolName }: { part: MessagePart; toolName: string }) {
|
|
||||||
const { input, output, state, errorText } = part as any;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tool>
|
|
||||||
<ToolHeader
|
|
||||||
title={toolName}
|
|
||||||
type={part.type as any}
|
|
||||||
state={state || "input-streaming"}
|
|
||||||
/>
|
|
||||||
<ToolContent>
|
|
||||||
{input && <ToolInput input={input} />}
|
|
||||||
<ToolOutput output={output} errorText={errorText} />
|
|
||||||
</ToolContent>
|
|
||||||
</Tool>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToolLoadingIndicator({ toolName }: { toolName: string }) {
|
|
||||||
return (
|
|
||||||
<div className="mb-4 flex items-center gap-2 text-muted-foreground text-sm">
|
|
||||||
<Loader size={14} />
|
|
||||||
<span>Running {toolName}...</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RenderTool({ part }: { part: MessagePart }) {
|
|
||||||
const toolName = extractToolName(part);
|
|
||||||
const state = (part as any).state;
|
|
||||||
const isComplete = state === "output-available";
|
|
||||||
|
|
||||||
const CustomRenderer = (
|
|
||||||
toolComponents as Record<
|
|
||||||
string,
|
|
||||||
React.ComponentType<{ part: MessagePart; toolName: string }>
|
|
||||||
>
|
|
||||||
)[toolName];
|
|
||||||
if (CustomRenderer) {
|
|
||||||
return <CustomRenderer part={part} toolName={toolName} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isComplete && !(part as any).input) {
|
|
||||||
return <ToolLoadingIndicator toolName={toolName} />;
|
|
||||||
}
|
|
||||||
return <DefaultToolRenderer part={part} toolName={toolName} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ToolPartRenderer({ part }: { part: MessagePart }) {
|
|
||||||
return <RenderTool part={part} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MessagePartRenderer({ part }: { part: MessagePart }) {
|
|
||||||
if (part.type.startsWith("tool-") || part.type === "dynamic-tool") {
|
|
||||||
return <RenderTool part={part} />;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { UIMessage } from "ai";
|
import { isToolUIPart, type ChatStatus, type UIMessage } from "ai";
|
||||||
import {
|
import {
|
||||||
Conversation,
|
Conversation,
|
||||||
ConversationContent,
|
ConversationContent,
|
||||||
@@ -19,14 +19,14 @@ import {
|
|||||||
ReasoningTrigger,
|
ReasoningTrigger,
|
||||||
} from "@/components/ai-elements/reasoning";
|
} from "@/components/ai-elements/reasoning";
|
||||||
import { Loader } from "@/components/ai-elements/loader";
|
import { Loader } from "@/components/ai-elements/loader";
|
||||||
import { ToolPartRenderer, getTextContent, getReasoningPart } from "./ChatMessageParts";
|
import { ToolPartRenderer } from "@/components/tools/tool-part-renderer";
|
||||||
|
import { getTextContent, getReasoningPart } from "@/lib/chat/messages";
|
||||||
|
|
||||||
interface ChatMessagesProps {
|
interface ChatMessagesProps {
|
||||||
messages: UIMessage[];
|
messages: UIMessage[];
|
||||||
isLoading?: boolean;
|
status?: ChatStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function LoadingIndicator() {
|
function LoadingIndicator() {
|
||||||
return (
|
return (
|
||||||
<Message from="assistant">
|
<Message from="assistant">
|
||||||
@@ -37,8 +37,7 @@ function LoadingIndicator() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
|
export function ChatMessages({ messages, status = "ready" }: ChatMessagesProps) {
|
||||||
const isToolPart = (type: string) => type === "file" || type.startsWith("tool-");
|
|
||||||
const hasReasoningPart = (parts: typeof messages[0]["parts"]) =>
|
const hasReasoningPart = (parts: typeof messages[0]["parts"]) =>
|
||||||
parts.some((p) => p.type === "reasoning" && p.text);
|
parts.some((p) => p.type === "reasoning" && p.text);
|
||||||
|
|
||||||
@@ -46,26 +45,31 @@ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
|
|||||||
(message) =>
|
(message) =>
|
||||||
message.role === "user" ||
|
message.role === "user" ||
|
||||||
getTextContent(message.parts).trim() !== "" ||
|
getTextContent(message.parts).trim() !== "" ||
|
||||||
message.parts.some((p) => isToolPart(p.type)) ||
|
message.parts.some((p) => p.type === "file" || isToolUIPart(p)) ||
|
||||||
hasReasoningPart(message.parts)
|
hasReasoningPart(message.parts)
|
||||||
);
|
);
|
||||||
|
|
||||||
const lastMessage = messages[messages.length - 1];
|
const lastMessage = messages[messages.length - 1];
|
||||||
const hasContent =
|
const hasContent =
|
||||||
getTextContent(lastMessage?.parts || []).trim() !== "" ||
|
getTextContent(lastMessage?.parts || []).trim() !== "" ||
|
||||||
lastMessage?.parts.some((p) => isToolPart(p.type)) ||
|
lastMessage?.parts.some((p) => p.type === "file" || isToolUIPart(p)) ||
|
||||||
hasReasoningPart(lastMessage?.parts || []);
|
hasReasoningPart(lastMessage?.parts || []);
|
||||||
const isStreaming = lastMessage?.role === "assistant" && hasContent;
|
const isLoading = status === "submitted" || status === "streaming";
|
||||||
const showLoading = isLoading && !isStreaming;
|
const showLoading =
|
||||||
|
isLoading && !(lastMessage?.role === "assistant" && hasContent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Conversation>
|
<Conversation>
|
||||||
<ConversationContent className="mx-auto max-w-3xl px-4 pb-4">
|
<ConversationContent className="mx-auto max-w-3xl px-4 pb-4">
|
||||||
{visibleMessages.map((message) => {
|
{visibleMessages.map((message) => {
|
||||||
|
const isStreaming =
|
||||||
|
status === "streaming" &&
|
||||||
|
message.role === "assistant" &&
|
||||||
|
message.id === lastMessage?.id;
|
||||||
const textContent = getTextContent(message.parts);
|
const textContent = getTextContent(message.parts);
|
||||||
const reasoningPart = getReasoningPart(message.parts);
|
const reasoningPart = getReasoningPart(message.parts);
|
||||||
const fileParts = message.parts.filter((p) => p.type === "file");
|
const fileParts = message.parts.filter((p) => p.type === "file");
|
||||||
const toolParts = message.parts.filter((p) => p.type.startsWith("tool-"));
|
const toolParts = message.parts.filter((p) => isToolUIPart(p));
|
||||||
return (
|
return (
|
||||||
<Message key={message.id} from={message.role}>
|
<Message key={message.id} from={message.role}>
|
||||||
{fileParts.length > 0 && (
|
{fileParts.length > 0 && (
|
||||||
@@ -80,7 +84,9 @@ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{message.role === "assistant" && reasoningPart && (
|
{message.role === "assistant" && reasoningPart && (
|
||||||
<Reasoning isStreaming={reasoningPart.state !== "done"}>
|
<Reasoning
|
||||||
|
isStreaming={isStreaming && reasoningPart.state === "streaming"}
|
||||||
|
>
|
||||||
<ReasoningTrigger />
|
<ReasoningTrigger />
|
||||||
<ReasoningContent>{reasoningPart.text || ""}</ReasoningContent>
|
<ReasoningContent>{reasoningPart.text || ""}</ReasoningContent>
|
||||||
</Reasoning>
|
</Reasoning>
|
||||||
@@ -92,7 +98,16 @@ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
|
|||||||
|
|
||||||
{textContent && (
|
{textContent && (
|
||||||
<MessageContent from={message.role}>
|
<MessageContent from={message.role}>
|
||||||
<MessageResponse>{textContent}</MessageResponse>
|
<MessageResponse
|
||||||
|
mode={message.role === "assistant" ? "streaming" : "static"}
|
||||||
|
isAnimating={
|
||||||
|
isStreaming && message.parts.some(
|
||||||
|
(part) => part.type === "text" && part.state === "streaming"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{textContent}
|
||||||
|
</MessageResponse>
|
||||||
</MessageContent>
|
</MessageContent>
|
||||||
)}
|
)}
|
||||||
</Message>
|
</Message>
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react';
|
||||||
|
import {
|
||||||
|
ModelSelector,
|
||||||
|
ModelSelectorContent,
|
||||||
|
ModelSelectorEmpty,
|
||||||
|
ModelSelectorGroup,
|
||||||
|
ModelSelectorInput,
|
||||||
|
ModelSelectorItem,
|
||||||
|
ModelSelectorList,
|
||||||
|
ModelSelectorName,
|
||||||
|
ModelSelectorTrigger,
|
||||||
|
} from '@/components/ai-elements/model-selector';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { openRouterModels, type OpenRouterModelId } from '@/lib/openrouter';
|
||||||
|
|
||||||
|
const providers: Record<string, string> = {
|
||||||
|
anthropic: 'Anthropic',
|
||||||
|
openai: 'OpenAI',
|
||||||
|
google: 'Google',
|
||||||
|
'x-ai': 'SpaceXAI',
|
||||||
|
'z-ai': 'Z.ai',
|
||||||
|
moonshotai: 'MoonshotAI',
|
||||||
|
minimax: 'MiniMax',
|
||||||
|
deepseek: 'DeepSeek',
|
||||||
|
meta: 'Meta',
|
||||||
|
qwen: 'Qwen',
|
||||||
|
};
|
||||||
|
|
||||||
|
const modelGroups = Object.entries(providers).map(([id, name]) => ({
|
||||||
|
name,
|
||||||
|
models: openRouterModels.filter((model) => model.id.startsWith(`${id}/`)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface ChatModelSelectorProps {
|
||||||
|
value: OpenRouterModelId;
|
||||||
|
onChange: (modelId: OpenRouterModelId) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatModelSelector({ value, onChange, disabled }: ChatModelSelectorProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const selectedModel = openRouterModels.find((model) => model.id === value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModelSelector open={open} onOpenChange={setOpen}>
|
||||||
|
<ModelSelectorTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="min-w-0 max-w-[min(16rem,calc(100vw-10rem))] text-muted-foreground"
|
||||||
|
aria-label={`Select model, current model: ${selectedModel?.name}`}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<ModelSelectorName>{selectedModel?.name}</ModelSelectorName>
|
||||||
|
<RiArrowDownSLine className="size-4 shrink-0" aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</ModelSelectorTrigger>
|
||||||
|
<ModelSelectorContent title="Choose a model" className="max-w-[calc(100%-2rem)] sm:max-w-lg">
|
||||||
|
<ModelSelectorInput placeholder="Search models..." aria-label="Search models" className="pr-10" />
|
||||||
|
<ModelSelectorList className="max-h-[min(24rem,60dvh)]">
|
||||||
|
<ModelSelectorEmpty>No models found.</ModelSelectorEmpty>
|
||||||
|
{modelGroups.map((group) => (
|
||||||
|
<ModelSelectorGroup heading={group.name} key={group.name}>
|
||||||
|
{group.models.map((model) => (
|
||||||
|
<ModelSelectorItem
|
||||||
|
key={model.id}
|
||||||
|
value={model.id}
|
||||||
|
keywords={[model.name, group.name]}
|
||||||
|
onSelect={() => {
|
||||||
|
onChange(model.id);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModelSelectorName>{model.name}</ModelSelectorName>
|
||||||
|
{model.id === value && (
|
||||||
|
<>
|
||||||
|
<RiCheckLine className="ml-auto size-4" aria-hidden="true" />
|
||||||
|
<span className="sr-only">Selected</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModelSelectorItem>
|
||||||
|
))}
|
||||||
|
</ModelSelectorGroup>
|
||||||
|
))}
|
||||||
|
</ModelSelectorList>
|
||||||
|
</ModelSelectorContent>
|
||||||
|
</ModelSelector>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,72 +5,40 @@ import { ChatInput, type PromptInputMessage } from './ChatInput';
|
|||||||
import { Suggestion } from '@/components/ai-elements/suggestion';
|
import { Suggestion } from '@/components/ai-elements/suggestion';
|
||||||
import { RiOpenaiLine } from '@remixicon/react';
|
import { RiOpenaiLine } from '@remixicon/react';
|
||||||
import { Shimmer } from '@/components/ai-elements/shimmer';
|
import { Shimmer } from '@/components/ai-elements/shimmer';
|
||||||
|
import type { OpenRouterModelId } from '@/lib/openrouter';
|
||||||
|
import {
|
||||||
|
offlineSuggestions,
|
||||||
|
webSearchSuggestions,
|
||||||
|
type PromptSuggestion,
|
||||||
|
} from '@/config/suggestions';
|
||||||
|
|
||||||
export interface PromptSuggestion {
|
export type { PromptSuggestion } from '@/config/suggestions';
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatPromptProps {
|
interface ChatPromptProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onSubmit: (message: PromptInputMessage) => void;
|
onSubmit: (message: PromptInputMessage) => void;
|
||||||
|
modelId: OpenRouterModelId;
|
||||||
|
onModelChange: (modelId: OpenRouterModelId) => void;
|
||||||
|
onStop?: () => void;
|
||||||
status?: ChatStatus;
|
status?: ChatStatus;
|
||||||
suggestions?: PromptSuggestion[];
|
suggestions?: PromptSuggestion[];
|
||||||
webSearchEnabled?: boolean;
|
webSearchEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSuggestions: PromptSuggestion[] = [
|
|
||||||
{
|
|
||||||
key: 'Latest AI news',
|
|
||||||
value:
|
|
||||||
'What are the latest developments in AI in 2026? Search for recent news.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Research a company',
|
|
||||||
value: 'Research OpenAI: their latest products, funding, and news.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Fact check',
|
|
||||||
value:
|
|
||||||
'What is the current population of the world and how is it changing?',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Compare topics',
|
|
||||||
value:
|
|
||||||
'Compare the latest iPhone vs Samsung Galaxy flagship models with sources.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const offlineSuggestions: PromptSuggestion[] = [
|
|
||||||
{
|
|
||||||
key: 'Explain a concept',
|
|
||||||
value: 'Explain retrieval-augmented generation in plain language.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Compare approaches',
|
|
||||||
value: 'Compare REST and GraphQL, including when to choose each.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Draft an outline',
|
|
||||||
value: 'Draft an outline for a practical guide to evaluating AI agents.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Brainstorm ideas',
|
|
||||||
value: 'Brainstorm useful AI agent ideas for a small business.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function ChatPrompt({
|
export function ChatPrompt({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
modelId,
|
||||||
|
onModelChange,
|
||||||
|
onStop,
|
||||||
status = 'ready',
|
status = 'ready',
|
||||||
suggestions,
|
suggestions,
|
||||||
webSearchEnabled = false,
|
webSearchEnabled = false,
|
||||||
}: ChatPromptProps) {
|
}: ChatPromptProps) {
|
||||||
const displayedSuggestions =
|
const displayedSuggestions =
|
||||||
suggestions ?? (webSearchEnabled ? defaultSuggestions : offlineSuggestions);
|
suggestions ?? (webSearchEnabled ? webSearchSuggestions : offlineSuggestions);
|
||||||
|
|
||||||
const handleSuggestionClick = (suggestion: string) => {
|
const handleSuggestionClick = (suggestion: string) => {
|
||||||
onChange(suggestion);
|
onChange(suggestion);
|
||||||
@@ -99,6 +67,9 @@ export function ChatPrompt({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
|
modelId={modelId}
|
||||||
|
onModelChange={onModelChange}
|
||||||
|
onStop={onStop}
|
||||||
status={status}
|
status={status}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { RiCheckLine, RiSearchLine } from '@remixicon/react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from '@/components/ui/command';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import type { StoredChat } from '@/services/indexed-db/chat-store';
|
||||||
|
|
||||||
|
interface ChatSearchProps {
|
||||||
|
chats: StoredChat[];
|
||||||
|
activeChatId: string | null;
|
||||||
|
onSelectChat: (chatId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatSearch({ chats, activeChatId, onSelectChat }: ChatSearchProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button type="button" variant="ghost" size="icon" className="size-7" aria-label="Search chats">
|
||||||
|
<RiSearchLine className="size-5" aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-[calc(100%-2rem)] overflow-hidden p-0 sm:max-w-lg [&>button]:top-2.5">
|
||||||
|
<DialogTitle className="sr-only">Search chats</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
Search your saved chats by title and select one to continue the conversation.
|
||||||
|
</DialogDescription>
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search chats..." aria-label="Search chats" className="pr-10" />
|
||||||
|
<CommandList className="max-h-[min(24rem,60dvh)]">
|
||||||
|
<CommandEmpty>
|
||||||
|
{chats.length === 0 ? 'No chats yet. Start a new chat to get started.' : 'No chats found.'}
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup heading="Recent chats">
|
||||||
|
{chats.map((chat) => (
|
||||||
|
<CommandItem
|
||||||
|
key={chat.id}
|
||||||
|
value={chat.id}
|
||||||
|
keywords={[chat.title]}
|
||||||
|
onSelect={() => {
|
||||||
|
setOpen(false);
|
||||||
|
onSelectChat(chat.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="min-w-0 flex-1 truncate">{chat.title}</span>
|
||||||
|
{chat.id === activeChatId && (
|
||||||
|
<>
|
||||||
|
<RiCheckLine className="size-4" aria-hidden="true" />
|
||||||
|
<span className="sr-only">Current chat</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { PlusIcon, MoreHorizontalIcon, TrashIcon } from 'lucide-react';
|
import { RiAddLine, RiMoreLine, RiDeleteBinLine } from '@remixicon/react';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuAction,
|
SidebarMenuAction,
|
||||||
|
useSidebar,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -20,12 +21,10 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ChatSearch } from './ChatSearch';
|
||||||
|
import type { StoredChat } from '@/services/indexed-db/chat-store';
|
||||||
|
|
||||||
export interface Chat {
|
export type Chat = StoredChat;
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatSidebarProps {
|
interface ChatSidebarProps {
|
||||||
chats: Chat[];
|
chats: Chat[];
|
||||||
@@ -42,15 +41,29 @@ export function ChatSidebar({
|
|||||||
onNewChat,
|
onNewChat,
|
||||||
onDeleteChat,
|
onDeleteChat,
|
||||||
}: ChatSidebarProps) {
|
}: ChatSidebarProps) {
|
||||||
|
const { setOpenMobile } = useSidebar();
|
||||||
|
|
||||||
|
const selectChat = (chatId: string) => {
|
||||||
|
onSelectChat(chatId);
|
||||||
|
setOpenMobile(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
|
<div className="flex h-7 items-center justify-between gap-2 pl-2">
|
||||||
|
<span className="text-xl font-semibold tracking-tight">ChatGPT</span>
|
||||||
|
<ChatSearch chats={chats} activeChatId={activeChatId} onSelectChat={selectChat} />
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={onNewChat}
|
onClick={() => {
|
||||||
|
onNewChat();
|
||||||
|
setOpenMobile(false);
|
||||||
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full justify-start gap-2"
|
className="mt-2 w-full justify-start gap-2"
|
||||||
>
|
>
|
||||||
<PlusIcon className="size-4" />
|
<RiAddLine className="size-4" />
|
||||||
New Chat
|
New Chat
|
||||||
</Button>
|
</Button>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
@@ -63,23 +76,22 @@ export function ChatSidebar({
|
|||||||
<SidebarMenuItem key={chat.id}>
|
<SidebarMenuItem key={chat.id}>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
isActive={chat.id === activeChatId}
|
isActive={chat.id === activeChatId}
|
||||||
onClick={() => onSelectChat(chat.id)}
|
onClick={() => selectChat(chat.id)}
|
||||||
tooltip={chat.title}
|
tooltip={chat.title}
|
||||||
>
|
>
|
||||||
<span>{chat.title}</span>
|
<span>{chat.title}</span>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuAction showOnHover>
|
<SidebarMenuAction showOnHover aria-label={`Options for ${chat.title}`}>
|
||||||
<MoreHorizontalIcon className="size-4" />
|
<RiMoreLine className="size-4" />
|
||||||
</SidebarMenuAction>
|
</SidebarMenuAction>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="right" align="start">
|
<DropdownMenuContent side="right" align="start">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => onDeleteChat(chat.id)}
|
onClick={() => onDeleteChat(chat.id)}
|
||||||
className=""
|
|
||||||
>
|
>
|
||||||
<TrashIcon className="size-4" />
|
<RiDeleteBinLine className="size-4" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
export { ChatAgent } from "./ChatAgent";
|
export { ChatAgent } from "./ChatAgent";
|
||||||
export { ChatInput } from "./ChatInput";
|
export { ChatInput } from "./ChatInput";
|
||||||
export { ChatMessage } from "./ChatMessage";
|
|
||||||
export { ChatMessages } from "./ChatMessages";
|
export { ChatMessages } from "./ChatMessages";
|
||||||
export { ChatPrompt } from "./ChatPrompt";
|
export { ChatPrompt } from "./ChatPrompt";
|
||||||
export { ChatSidebar } from "./ChatSidebar";
|
export { ChatSidebar } from "./ChatSidebar";
|
||||||
export type { Chat } from "./ChatSidebar";
|
export type { Chat } from "./ChatSidebar";
|
||||||
export type { Message } from "./ChatMessage";
|
export type { UIMessage as Message } from "ai";
|
||||||
export type { PromptSuggestion } from "./ChatPrompt";
|
export type { PromptSuggestion } from "./ChatPrompt";
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { ComponentType } from 'react';
|
||||||
|
import { FirecrawlWebFetchTool } from './firecrawl-web-fetch-tool';
|
||||||
|
import { FirecrawlWebSearchTool } from './firecrawl-web-search-tool';
|
||||||
|
import type { ToolComponentProps } from './types';
|
||||||
|
|
||||||
|
// Add a renderer here for each tool that needs a custom presentation.
|
||||||
|
export const toolComponents: Record<string, ComponentType<ToolComponentProps> | undefined> = {
|
||||||
|
firecrawlWebSearch: FirecrawlWebSearchTool,
|
||||||
|
firecrawlWebFetch: FirecrawlWebFetchTool,
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getToolName } from 'ai';
|
||||||
|
import {
|
||||||
|
Tool,
|
||||||
|
ToolContent,
|
||||||
|
ToolHeader,
|
||||||
|
ToolInput,
|
||||||
|
ToolOutput,
|
||||||
|
} from '@/components/ai-elements/tool';
|
||||||
|
import { Loader } from '@/components/ai-elements/loader';
|
||||||
|
import { toolComponents } from './registry';
|
||||||
|
import type { ToolComponentProps } from './types';
|
||||||
|
|
||||||
|
export function ToolPartRenderer({ part }: Pick<ToolComponentProps, 'part'>) {
|
||||||
|
const toolName = getToolName(part);
|
||||||
|
const CustomRenderer = toolComponents[toolName];
|
||||||
|
|
||||||
|
if (CustomRenderer) {
|
||||||
|
return <CustomRenderer part={part} toolName={toolName} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.state === 'input-streaming' && !part.input) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4 flex items-center gap-2 text-muted-foreground text-sm">
|
||||||
|
<Loader size={14} />
|
||||||
|
<span>Running {toolName}...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tool>
|
||||||
|
<ToolHeader
|
||||||
|
title={toolName}
|
||||||
|
type={part.type === 'dynamic-tool' ? `tool-${toolName}` : part.type}
|
||||||
|
state={part.state}
|
||||||
|
/>
|
||||||
|
<ToolContent>
|
||||||
|
{part.input != null && <ToolInput input={part.input} />}
|
||||||
|
<ToolOutput output={part.output} errorText={part.errorText} />
|
||||||
|
</ToolContent>
|
||||||
|
</Tool>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { UIMessage } from "ai";
|
import type { DynamicToolUIPart, ToolUIPart } from "ai";
|
||||||
|
|
||||||
type MessagePart = UIMessage["parts"][number];
|
|
||||||
|
|
||||||
export interface ToolComponentProps {
|
export interface ToolComponentProps {
|
||||||
part: MessagePart;
|
part: ToolUIPart | DynamicToolUIPart;
|
||||||
toolName: string;
|
toolName: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
import { ChevronDownIcon } from "lucide-react"
|
import { RiArrowDownSLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ function AccordionTrigger({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
<RiArrowDownSLine className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||||
</AccordionPrimitive.Trigger>
|
</AccordionPrimitive.Trigger>
|
||||||
</AccordionPrimitive.Header>
|
</AccordionPrimitive.Header>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
import { RiArrowRightSLine, RiMoreLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ function BreadcrumbSeparator({
|
|||||||
className={cn("[&>svg]:size-3.5", className)}
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <ChevronRight />}
|
{children ?? <RiArrowRightSLine />}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ function BreadcrumbEllipsis({
|
|||||||
className={cn("flex size-9 items-center justify-center", className)}
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="size-4" />
|
<RiMoreLine className="size-4" />
|
||||||
<span className="sr-only">More</span>
|
<span className="sr-only">More</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
RiArrowDownSLine,
|
||||||
ChevronLeftIcon,
|
RiArrowLeftSLine,
|
||||||
ChevronRightIcon,
|
RiArrowRightSLine,
|
||||||
} from "lucide-react"
|
} from "@remixicon/react"
|
||||||
import {
|
import {
|
||||||
DayPicker,
|
DayPicker,
|
||||||
getDefaultClassNames,
|
getDefaultClassNames,
|
||||||
@@ -145,13 +145,13 @@ function Calendar({
|
|||||||
Chevron: ({ className, orientation, ...props }) => {
|
Chevron: ({ className, orientation, ...props }) => {
|
||||||
if (orientation === "left") {
|
if (orientation === "left") {
|
||||||
return (
|
return (
|
||||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
<RiArrowLeftSLine className={cn("size-4", className)} {...props} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orientation === "right") {
|
if (orientation === "right") {
|
||||||
return (
|
return (
|
||||||
<ChevronRightIcon
|
<RiArrowRightSLine
|
||||||
className={cn("size-4", className)}
|
className={cn("size-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -159,7 +159,7 @@ function Calendar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
<RiArrowDownSLine className={cn("size-4", className)} {...props} />
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
DayButton: CalendarDayButton,
|
DayButton: CalendarDayButton,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
|||||||
import useEmblaCarousel, {
|
import useEmblaCarousel, {
|
||||||
type UseEmblaCarouselType,
|
type UseEmblaCarouselType,
|
||||||
} from "embla-carousel-react"
|
} from "embla-carousel-react"
|
||||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
import { RiArrowLeftLine, RiArrowRightLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -77,10 +77,10 @@ function Carousel({
|
|||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
const handleKeyDown = React.useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.key === "ArrowLeft") {
|
if (event.key === "RiArrowLeftLine") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
scrollPrev()
|
scrollPrev()
|
||||||
} else if (event.key === "ArrowRight") {
|
} else if (event.key === "RiArrowRightLine") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
scrollNext()
|
scrollNext()
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ function CarouselPrevious({
|
|||||||
onClick={scrollPrev}
|
onClick={scrollPrev}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowLeft />
|
<RiArrowLeftLine />
|
||||||
<span className="sr-only">Previous slide</span>
|
<span className="sr-only">Previous slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
@@ -225,7 +225,7 @@ function CarouselNext({
|
|||||||
onClick={scrollNext}
|
onClick={scrollNext}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowRight />
|
<RiArrowRightLine />
|
||||||
<span className="sr-only">Next slide</span>
|
<span className="sr-only">Next slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
import { CheckIcon } from "lucide-react"
|
import { RiCheckLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ function Checkbox({
|
|||||||
data-slot="checkbox-indicator"
|
data-slot="checkbox-indicator"
|
||||||
className="grid place-content-center text-current transition-none"
|
className="grid place-content-center text-current transition-none"
|
||||||
>
|
>
|
||||||
<CheckIcon className="size-3.5" />
|
<RiCheckLine className="size-3.5" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
|
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
|
||||||
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"
|
import { RiCheckLine, RiArrowDownSLine, RiCloseLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -31,7 +31,7 @@ function ComboboxTrigger({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronDownIcon
|
<RiArrowDownSLine
|
||||||
data-slot="combobox-trigger-icon"
|
data-slot="combobox-trigger-icon"
|
||||||
className="text-muted-foreground pointer-events-none size-4"
|
className="text-muted-foreground pointer-events-none size-4"
|
||||||
/>
|
/>
|
||||||
@@ -47,7 +47,7 @@ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
|||||||
className={cn(className)}
|
className={cn(className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<XIcon className="pointer-events-none" />
|
<RiCloseLine className="pointer-events-none" />
|
||||||
</ComboboxPrimitive.Clear>
|
</ComboboxPrimitive.Clear>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ function ComboboxItem({
|
|||||||
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CheckIcon className="pointer-events-none size-4 pointer-coarse:size-5" />
|
<RiCheckLine className="pointer-events-none size-4 pointer-coarse:size-5" />
|
||||||
</ComboboxPrimitive.ItemIndicator>
|
</ComboboxPrimitive.ItemIndicator>
|
||||||
</ComboboxPrimitive.Item>
|
</ComboboxPrimitive.Item>
|
||||||
)
|
)
|
||||||
@@ -265,7 +265,7 @@ function ComboboxChip({
|
|||||||
className="-ml-1 opacity-50 hover:opacity-100"
|
className="-ml-1 opacity-50 hover:opacity-100"
|
||||||
data-slot="combobox-chip-remove"
|
data-slot="combobox-chip-remove"
|
||||||
>
|
>
|
||||||
<XIcon className="pointer-events-none" />
|
<RiCloseLine className="pointer-events-none" />
|
||||||
</ComboboxPrimitive.ChipRemove>
|
</ComboboxPrimitive.ChipRemove>
|
||||||
)}
|
)}
|
||||||
</ComboboxPrimitive.Chip>
|
</ComboboxPrimitive.Chip>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
import { SearchIcon } from "lucide-react"
|
import { RiSearchLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import {
|
import {
|
||||||
@@ -69,7 +69,7 @@ function CommandInput({
|
|||||||
data-slot="command-input-wrapper"
|
data-slot="command-input-wrapper"
|
||||||
className="flex h-9 items-center gap-2 border-b px-3"
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
>
|
>
|
||||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
<RiSearchLine className="size-4 shrink-0 opacity-50" />
|
||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
data-slot="command-input"
|
data-slot="command-input"
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
import { RiCheckLine, RiArrowRightSLine, RiCircleLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ function ContextMenuSubTrigger({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto" />
|
<RiArrowRightSLine className="ml-auto" />
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ function ContextMenuCheckboxItem({
|
|||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<RiCheckLine className="size-4" />
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
@@ -176,7 +176,7 @@ function ContextMenuRadioItem({
|
|||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
<CircleIcon className="size-2 fill-current" />
|
<RiCircleLine className="size-2 fill-current" />
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const DialogContent = React.forwardRef<
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-50 grid w-full max-w-lg gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg',
|
'relative z-50 grid w-full max-w-lg gap-4 border border-border bg-background p-6 shadow-lg duration-200 rounded-lg',
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||||
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
import { RiCheckLine, RiArrowRightSLine, RiCircleLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ function DropdownMenuCheckboxItem({
|
|||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<RiCheckLine className="size-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
@@ -135,7 +135,7 @@ function DropdownMenuRadioItem({
|
|||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CircleIcon className="size-2 fill-current" />
|
<RiCircleLine className="size-2 fill-current" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
@@ -217,7 +217,7 @@ function DropdownMenuSubTrigger({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
<RiArrowRightSLine className="ml-auto size-4" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { OTPInput, OTPInputContext } from "input-otp"
|
import { OTPInput, OTPInputContext } from "input-otp"
|
||||||
import { MinusIcon } from "lucide-react"
|
import { RiSubtractLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ function InputOTPSlot({
|
|||||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||||
<MinusIcon />
|
<RiSubtractLine />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
import { RiCheckLine, RiArrowRightSLine, RiCircleLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ function MenubarCheckboxItem({
|
|||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<MenubarPrimitive.ItemIndicator>
|
<MenubarPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<RiCheckLine className="size-4" />
|
||||||
</MenubarPrimitive.ItemIndicator>
|
</MenubarPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
@@ -153,7 +153,7 @@ function MenubarRadioItem({
|
|||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<MenubarPrimitive.ItemIndicator>
|
<MenubarPrimitive.ItemIndicator>
|
||||||
<CircleIcon className="size-2 fill-current" />
|
<RiCircleLine className="size-2 fill-current" />
|
||||||
</MenubarPrimitive.ItemIndicator>
|
</MenubarPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
@@ -235,7 +235,7 @@ function MenubarSubTrigger({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
<RiArrowRightSLine className="ml-auto h-4 w-4" />
|
||||||
</MenubarPrimitive.SubTrigger>
|
</MenubarPrimitive.SubTrigger>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { ChevronDownIcon } from "lucide-react"
|
import { RiArrowDownSLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ function NativeSelect({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<ChevronDownIcon
|
<RiArrowDownSLine
|
||||||
className="text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
|
className="text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
data-slot="native-select-icon"
|
data-slot="native-select-icon"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||||
import { cva } from "class-variance-authority"
|
import { cva } from "class-variance-authority"
|
||||||
import { ChevronDownIcon } from "lucide-react"
|
import { RiArrowDownSLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ function NavigationMenuTrigger({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}{" "}
|
{children}{" "}
|
||||||
<ChevronDownIcon
|
<RiArrowDownSLine
|
||||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {
|
import {
|
||||||
ChevronLeftIcon,
|
RiArrowLeftSLine,
|
||||||
ChevronRightIcon,
|
RiArrowRightSLine,
|
||||||
MoreHorizontalIcon,
|
RiMoreLine,
|
||||||
} from "lucide-react"
|
} from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { buttonVariants, type Button } from "@/components/ui/button"
|
import { buttonVariants, type Button } from "@/components/ui/button"
|
||||||
@@ -76,7 +76,7 @@ function PaginationPrevious({
|
|||||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon />
|
<RiArrowLeftSLine />
|
||||||
<span className="hidden sm:block">Previous</span>
|
<span className="hidden sm:block">Previous</span>
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
)
|
||||||
@@ -94,7 +94,7 @@ function PaginationNext({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:block">Next</span>
|
<span className="hidden sm:block">Next</span>
|
||||||
<ChevronRightIcon />
|
<RiArrowRightSLine />
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ function PaginationEllipsis({
|
|||||||
className={cn("flex size-9 items-center justify-center", className)}
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon className="size-4" />
|
<RiMoreLine className="size-4" />
|
||||||
<span className="sr-only">More pages</span>
|
<span className="sr-only">More pages</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||||
import { CircleIcon } from "lucide-react"
|
import { RiCircleFill } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ function RadioGroupItem({
|
|||||||
data-slot="radio-group-indicator"
|
data-slot="radio-group-indicator"
|
||||||
className="relative flex items-center justify-center"
|
className="relative flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
<RiCircleFill className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
</RadioGroupPrimitive.Indicator>
|
</RadioGroupPrimitive.Indicator>
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { GripVerticalIcon } from "lucide-react"
|
import { RiDraggable } from "@remixicon/react"
|
||||||
import * as ResizablePrimitive from "react-resizable-panels"
|
import * as ResizablePrimitive from "react-resizable-panels"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
@@ -46,7 +46,7 @@ function ResizableHandle({
|
|||||||
>
|
>
|
||||||
{withHandle && (
|
{withHandle && (
|
||||||
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||||
<GripVerticalIcon className="size-2.5" />
|
<RiDraggable className="size-2.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ResizablePrimitive.Separator>
|
</ResizablePrimitive.Separator>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
import { RiCheckLine, RiArrowDownSLine, RiArrowUpSLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ function SelectTrigger({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<SelectPrimitive.Icon asChild>
|
<SelectPrimitive.Icon asChild>
|
||||||
<ChevronDownIcon className="size-4 opacity-50" />
|
<RiArrowDownSLine className="size-4 opacity-50" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
)
|
)
|
||||||
@@ -119,7 +119,7 @@ function SelectItem({
|
|||||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||||
>
|
>
|
||||||
<SelectPrimitive.ItemIndicator>
|
<SelectPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<RiCheckLine className="size-4" />
|
||||||
</SelectPrimitive.ItemIndicator>
|
</SelectPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
@@ -153,7 +153,7 @@ function SelectScrollUpButton({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronUpIcon className="size-4" />
|
<RiArrowUpSLine className="size-4" />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ function SelectScrollDownButton({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon className="size-4" />
|
<RiArrowDownSLine className="size-4" />
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||||
import { XIcon } from "lucide-react"
|
import { RiCloseLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ function SheetContent({
|
|||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||||
<XIcon className="size-4" />
|
<RiCloseLine className="size-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import { PanelLeftIcon } from "lucide-react"
|
import { RiLayoutLeftLine } from "@remixicon/react"
|
||||||
|
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
@@ -273,7 +273,7 @@ function SidebarTrigger({
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<PanelLeftIcon />
|
<RiLayoutLeftLine className="size-5" aria-hidden="true" />
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
<span className="sr-only">Toggle Sidebar</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
+11
-11
@@ -1,12 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CircleCheckIcon,
|
RiCheckboxCircleLine,
|
||||||
InfoIcon,
|
RiInformationLine,
|
||||||
Loader2Icon,
|
RiLoader4Line,
|
||||||
OctagonXIcon,
|
RiCloseCircleLine,
|
||||||
TriangleAlertIcon,
|
RiErrorWarningLine,
|
||||||
} from "lucide-react"
|
} from "@remixicon/react"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||||
|
|
||||||
@@ -18,11 +18,11 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps["theme"]}
|
||||||
className="toaster group"
|
className="toaster group"
|
||||||
icons={{
|
icons={{
|
||||||
success: <CircleCheckIcon className="size-4" />,
|
success: <RiCheckboxCircleLine className="size-4" />,
|
||||||
info: <InfoIcon className="size-4" />,
|
info: <RiInformationLine className="size-4" />,
|
||||||
warning: <TriangleAlertIcon className="size-4" />,
|
warning: <RiErrorWarningLine className="size-4" />,
|
||||||
error: <OctagonXIcon className="size-4" />,
|
error: <RiCloseCircleLine className="size-4" />,
|
||||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
loading: <RiLoader4Line className="size-4 animate-spin" />,
|
||||||
}}
|
}}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Loader2Icon } from "lucide-react"
|
import { RiLoader4Line } from "@remixicon/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
function Spinner({ className, ...props }: React.ComponentProps<typeof RiLoader4Line>) {
|
||||||
return (
|
return (
|
||||||
<Loader2Icon
|
<RiLoader4Line
|
||||||
role="status"
|
role="status"
|
||||||
aria-label="Loading"
|
aria-label="Loading"
|
||||||
className={cn("size-4 animate-spin", className)}
|
className={cn("size-4 animate-spin", className)}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
export interface PromptSuggestion {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const webSearchSuggestions: PromptSuggestion[] = [
|
||||||
|
{
|
||||||
|
key: 'Latest AI news',
|
||||||
|
value:
|
||||||
|
'What are the latest developments in AI? Search for recent news.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Research a company',
|
||||||
|
value: 'Research OpenAI: their latest products, funding, and news.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Fact check',
|
||||||
|
value:
|
||||||
|
'What is the current population of the world and how is it changing?',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Compare topics',
|
||||||
|
value:
|
||||||
|
'Compare the latest iPhone vs Samsung Galaxy flagship models with sources.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const offlineSuggestions: PromptSuggestion[] = [
|
||||||
|
{
|
||||||
|
key: 'Explain a concept',
|
||||||
|
value: 'Explain retrieval-augmented generation in plain language.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Compare approaches',
|
||||||
|
value: 'Compare REST and GraphQL, including when to choose each.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Draft an outline',
|
||||||
|
value: 'Draft an outline for a practical guide to evaluating AI agents.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Brainstorm ideas',
|
||||||
|
value: 'Brainstorm useful AI agent ideas for a small business.',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import { useChat } from '@ai-sdk/react';
|
||||||
|
import { DefaultChatTransport, type ChatStatus, type UIMessage } from 'ai';
|
||||||
|
import { DEFAULT_OPENROUTER_MODEL_ID, type OpenRouterModelId } from '@/lib/openrouter';
|
||||||
|
import type { PromptInputMessage } from '@/components/ai-elements/prompt-input';
|
||||||
|
import {
|
||||||
|
createChat,
|
||||||
|
deleteChat,
|
||||||
|
getMessages,
|
||||||
|
listChats,
|
||||||
|
saveMessages,
|
||||||
|
type StoredChat,
|
||||||
|
} from '@/services/indexed-db/chat-store';
|
||||||
|
|
||||||
|
async function convertToDataUrl(url: string): Promise<string> {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error('Could not read the selected file');
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
if (typeof reader.result === 'string') {
|
||||||
|
resolve(reader.result);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Could not encode the selected file'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = () => reject(reader.error);
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChatSession(defaultModelId: OpenRouterModelId = DEFAULT_OPENROUTER_MODEL_ID) {
|
||||||
|
const [modelId, setModelId] = useState<OpenRouterModelId>(defaultModelId);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [chatId, setChatId] = useState<string | null>(null);
|
||||||
|
const [chats, setChats] = useState<StoredChat[]>([]);
|
||||||
|
const chatIdRef = useRef(chatId);
|
||||||
|
const pendingChatIdRef = useRef<string | null>(null);
|
||||||
|
chatIdRef.current = chatId;
|
||||||
|
|
||||||
|
const fetchChats = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setChats(await listChats());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch chats:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const transportRef = useRef(
|
||||||
|
new DefaultChatTransport({
|
||||||
|
api: '/api/chat',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
messages: chatMessages,
|
||||||
|
setMessages: setChatMessages,
|
||||||
|
sendMessage,
|
||||||
|
stop,
|
||||||
|
error: aiError,
|
||||||
|
} = useChat({
|
||||||
|
transport: transportRef.current,
|
||||||
|
onFinish: async ({ messages }) => {
|
||||||
|
const activeChatId = pendingChatIdRef.current ?? chatIdRef.current;
|
||||||
|
if (!activeChatId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveMessages(activeChatId, messages);
|
||||||
|
await fetchChats();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save messages:', error);
|
||||||
|
} finally {
|
||||||
|
if (pendingChatIdRef.current === activeChatId) {
|
||||||
|
pendingChatIdRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Chat error:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isPreparingFiles, setIsPreparingFiles] = useState(false);
|
||||||
|
|
||||||
|
const chatStatus: ChatStatus = isPreparingFiles
|
||||||
|
? 'submitted'
|
||||||
|
: status === 'submitted' || status === 'streaming'
|
||||||
|
? status
|
||||||
|
: 'ready';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchChats();
|
||||||
|
}, [fetchChats]);
|
||||||
|
|
||||||
|
const handleSelectChat = useCallback(
|
||||||
|
async (selectedChatId: string) => {
|
||||||
|
try {
|
||||||
|
const messages = await getMessages(selectedChatId);
|
||||||
|
chatIdRef.current = selectedChatId;
|
||||||
|
setChatId(selectedChatId);
|
||||||
|
|
||||||
|
setChatMessages(messages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load chat:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setChatMessages]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNewChat = useCallback(() => {
|
||||||
|
chatIdRef.current = null;
|
||||||
|
setChatId(null);
|
||||||
|
setChatMessages([]);
|
||||||
|
setInputValue('');
|
||||||
|
}, [setChatMessages]);
|
||||||
|
|
||||||
|
const handleDeleteChat = useCallback(
|
||||||
|
async (deletedChatId: string) => {
|
||||||
|
try {
|
||||||
|
await deleteChat(deletedChatId);
|
||||||
|
setChats((prev) => prev.filter((c) => c.id !== deletedChatId));
|
||||||
|
if (chatId === deletedChatId) {
|
||||||
|
chatIdRef.current = null;
|
||||||
|
setChatId(null);
|
||||||
|
setChatMessages([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete chat:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[chatId, setChatMessages]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSendMessage = useCallback(
|
||||||
|
async (message: PromptInputMessage) => {
|
||||||
|
const trimmedValue = message.text?.trim();
|
||||||
|
if (!trimmedValue && !message.files?.length) return;
|
||||||
|
if (
|
||||||
|
status === 'submitted' ||
|
||||||
|
status === 'streaming' ||
|
||||||
|
isPreparingFiles
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let activeChatId = chatIdRef.current;
|
||||||
|
|
||||||
|
if (!chatIdRef.current) {
|
||||||
|
const newChatId = crypto.randomUUID();
|
||||||
|
const title = trimmedValue ? trimmedValue.slice(0, 100) : 'New Chat';
|
||||||
|
|
||||||
|
activeChatId = newChatId;
|
||||||
|
chatIdRef.current = newChatId;
|
||||||
|
setChatId(newChatId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createChat(newChatId, title);
|
||||||
|
await fetchChats();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create chat:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: Array<
|
||||||
|
| { type: 'text'; text: string }
|
||||||
|
| { type: 'file'; url: string; mediaType: string; filename: string }
|
||||||
|
> = [];
|
||||||
|
|
||||||
|
if (trimmedValue) {
|
||||||
|
parts.push({ type: 'text', text: trimmedValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromptInput supplies files as base64 data URLs. Keep them inline so the
|
||||||
|
// demo never needs a remote storage bucket.
|
||||||
|
if (message.files?.length) {
|
||||||
|
setIsPreparingFiles(true);
|
||||||
|
for (const file of message.files) {
|
||||||
|
try {
|
||||||
|
const dataUrl = file.url.startsWith('data:')
|
||||||
|
? file.url
|
||||||
|
: await convertToDataUrl(file.url);
|
||||||
|
|
||||||
|
parts.push({
|
||||||
|
type: 'file',
|
||||||
|
url: dataUrl,
|
||||||
|
mediaType: file.mediaType || 'application/octet-stream',
|
||||||
|
filename: file.filename || 'file',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('File encoding error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsPreparingFiles(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length === 0) return;
|
||||||
|
|
||||||
|
const userMessage: UIMessage = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
role: 'user',
|
||||||
|
parts,
|
||||||
|
};
|
||||||
|
|
||||||
|
setInputValue('');
|
||||||
|
|
||||||
|
if (activeChatId) {
|
||||||
|
try {
|
||||||
|
await saveMessages(activeChatId, [...chatMessages, userMessage]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save user message:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingChatIdRef.current = activeChatId;
|
||||||
|
await sendMessage(userMessage, { body: { modelId } });
|
||||||
|
},
|
||||||
|
[
|
||||||
|
status,
|
||||||
|
sendMessage,
|
||||||
|
chatMessages,
|
||||||
|
isPreparingFiles,
|
||||||
|
fetchChats,
|
||||||
|
modelId,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStop = useCallback(() => {
|
||||||
|
stop();
|
||||||
|
}, [stop]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputValue,
|
||||||
|
setInputValue,
|
||||||
|
modelId,
|
||||||
|
setModelId,
|
||||||
|
chatId,
|
||||||
|
chats,
|
||||||
|
chatMessages,
|
||||||
|
status,
|
||||||
|
chatStatus,
|
||||||
|
aiError,
|
||||||
|
isPreparingFiles,
|
||||||
|
handleSelectChat,
|
||||||
|
handleNewChat,
|
||||||
|
handleDeleteChat,
|
||||||
|
handleSendMessage,
|
||||||
|
handleStop,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { UIMessage } from "ai";
|
||||||
|
|
||||||
|
type MessagePart = UIMessage["parts"][number];
|
||||||
|
type ReasoningPart = Extract<MessagePart, { type: "reasoning" }>;
|
||||||
|
|
||||||
|
export function getTextContent(parts: MessagePart[]): string {
|
||||||
|
return parts
|
||||||
|
.filter((p) => p.type === "text")
|
||||||
|
.map((p) => p.text || "")
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReasoningPart(parts: MessagePart[]): ReasoningPart | undefined {
|
||||||
|
return parts.find((p): p is ReasoningPart => p.type === "reasoning");
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
export const openRouterModels = [
|
||||||
|
{ id: 'anthropic/claude-opus-5', name: 'Claude Opus 5' },
|
||||||
|
{ id: 'anthropic/claude-opus-5-fast', name: 'Claude Opus 5 (Fast)' },
|
||||||
|
{ id: 'anthropic/claude-sonnet-5', name: 'Claude Sonnet 5' },
|
||||||
|
{ id: 'anthropic/claude-fable-5', name: 'Claude Fable 5' },
|
||||||
|
{ id: 'openai/gpt-6-astra-pro', name: 'GPT-6 Astra Pro' },
|
||||||
|
{ id: 'openai/gpt-5.6-luna', name: 'GPT-5.6 Luna' },
|
||||||
|
{ id: 'openai/gpt-5.6-luna-pro', name: 'GPT-5.6 Luna Pro' },
|
||||||
|
{ id: 'openai/gpt-5.6-sol', name: 'GPT-5.6 Sol' },
|
||||||
|
{ id: 'openai/gpt-5.6-sol-pro', name: 'GPT-5.6 Sol Pro' },
|
||||||
|
{ id: 'openai/gpt-5.6-terra', name: 'GPT-5.6 Terra' },
|
||||||
|
{ id: 'openai/gpt-5.6-terra-pro', name: 'GPT-5.6 Terra Pro' },
|
||||||
|
{ id: 'google/gemini-3.8-flash', name: 'Gemini 3.8 Flash' },
|
||||||
|
{ id: 'x-ai/grok-4.6', name: 'Grok 4.6' },
|
||||||
|
{ id: 'z-ai/glm-5.3', name: 'GLM 5.3' },
|
||||||
|
{ id: 'moonshotai/kimi-k3', name: 'Kimi K3' },
|
||||||
|
{ id: 'minimax/minimax-m3', name: 'MiniMax M3' },
|
||||||
|
{ id: 'deepseek/deepseek-v4-pro', name: 'DeepSeek V4 Pro 0423' },
|
||||||
|
{ id: 'deepseek/deepseek-v4-flash-0731', name: 'DeepSeek V4 Flash 0731' },
|
||||||
|
{ id: 'meta/muse-spark-1.1', name: 'Muse Spark 1.1' },
|
||||||
|
{ id: 'qwen/qwen3.8-max', name: 'Qwen3.8 Max' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type OpenRouterModelId = (typeof openRouterModels)[number]['id'];
|
||||||
|
|
||||||
|
export const DEFAULT_OPENROUTER_MODEL_ID: OpenRouterModelId = 'openai/gpt-5.6-luna-pro';
|
||||||
|
|
||||||
|
export function isOpenRouterModelId(value: unknown): value is OpenRouterModelId {
|
||||||
|
return typeof value === 'string' && openRouterModels.some((model) => model.id === value);
|
||||||
|
}
|
||||||
+16
-23
@@ -1,30 +1,23 @@
|
|||||||
const nextConfig = {
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
outputFileTracingRoot: process.cwd(),
|
outputFileTracingRoot: process.cwd(),
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
images: {
|
images: {
|
||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{ protocol: 'https', hostname: 'images.unsplash.com' },
|
||||||
protocol: 'https',
|
{ protocol: 'https', hostname: 'cdn.shopify.com' },
|
||||||
hostname: 'images.unsplash.com',
|
{ protocol: 'https', hostname: '**.frontend.co' },
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
protocol: 'https',
|
webpack: (config, { isServer }) => {
|
||||||
hostname: 'cdn.shopify.com',
|
config.cache = { type: 'memory' };
|
||||||
},
|
if (isServer) {
|
||||||
{
|
config.optimization = config.optimization || {};
|
||||||
protocol: 'https',
|
config.optimization.splitChunks = false;
|
||||||
hostname: '**.frontend.co',
|
}
|
||||||
},
|
return config;
|
||||||
],
|
},
|
||||||
},
|
|
||||||
webpack: (config, { isServer }) => {
|
|
||||||
config.cache = { type: "memory" };
|
|
||||||
if (isServer) {
|
|
||||||
config.optimization = config.optimization || {};
|
|
||||||
config.optimization.splitChunks = false;
|
|
||||||
} return config;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
+9
-14
@@ -1,20 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "nextjs-app",
|
"name": "nextjs-chatgpt-template",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --webpack",
|
"dev": "next dev --webpack",
|
||||||
"build": "next build --webpack",
|
"build": "next build --webpack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "node --test tests/*.test.mjs"
|
||||||
},
|
},
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^4.0.9",
|
|
||||||
"@ai-sdk/react": "^4.0.19",
|
"@ai-sdk/react": "^4.0.19",
|
||||||
"@base-ui/react": "^1.8.0",
|
"@base-ui/react": "^1.8.0",
|
||||||
"@mendable/firecrawl-js": "^4.12.1",
|
|
||||||
"@neondatabase/serverless": "^1.0.2",
|
|
||||||
"@next/swc-wasm-web": "16.2.10",
|
"@next/swc-wasm-web": "16.2.10",
|
||||||
"@openrouter/ai-sdk-provider": "^3.0.0",
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.3",
|
"@radix-ui/react-accordion": "^1.2.3",
|
||||||
@@ -44,6 +42,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.2",
|
"@radix-ui/react-toggle": "^1.1.2",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
|
"@radix-ui/react-use-controllable-state": "^1.2.6",
|
||||||
"@remixicon/react": "^4.9.0",
|
"@remixicon/react": "^4.9.0",
|
||||||
"@xyflow/react": "^12.10.0",
|
"@xyflow/react": "^12.10.0",
|
||||||
"ai": "^7.0.18",
|
"ai": "^7.0.18",
|
||||||
@@ -51,25 +50,17 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-orm": "^0.45.1",
|
|
||||||
"embla-carousel-react": "^8.5.2",
|
"embla-carousel-react": "^8.5.2",
|
||||||
"exa-js": "^2.4.0",
|
|
||||||
"framer-motion": "^12.0.0",
|
|
||||||
"gql-tag": "^1.0.1",
|
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.574.0",
|
|
||||||
"motion": "^12.33.0",
|
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"next": "16.2.10",
|
"next": "16.2.10",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"openai": "^6.17.0",
|
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-day-picker": "^9.5.0",
|
"react-day-picker": "^9.5.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.88.0",
|
"react-hook-form": "^7.88.0",
|
||||||
"react-is": "^19.3.0",
|
"react-is": "^19.3.0",
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"react-resizable-panels": "^4.5.8",
|
"react-resizable-panels": "^4.5.8",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.7.0",
|
||||||
"shiki": "^3.22.0",
|
"shiki": "^3.22.0",
|
||||||
@@ -89,7 +80,11 @@
|
|||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@types/react-is": "^19",
|
"@types/react-is": "^19",
|
||||||
|
"jiti": "^2.7.0",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.18.0+sha512.fcb8716fe7cd0eece141ffc18b92193a9df9204c1ba83189c288835223fc0bbe64af473bab0d5e9927a7daeb5caf2bb07eb2787cc9338ca040ea125f2a1f2f7e"
|
"packageManager": "yarn@4.18.0+sha512.fcb8716fe7cd0eece141ffc18b92193a9df9204c1ba83189c288835223fc0bbe64af473bab0d5e9927a7daeb5caf2bb07eb2787cc9338ca040ea125f2a1f2f7e",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export function getChatInstructions(webSearchEnabled: boolean) {
|
||||||
|
const today = new Date().toLocaleDateString('en-US', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
return webSearchEnabled
|
||||||
|
? `You are an expert ChatGPT Agent with real-time access to the internet.
|
||||||
|
|
||||||
|
Your capabilities:
|
||||||
|
- Search the web using Firecrawl to find up-to-date information
|
||||||
|
- Fetch and read full web pages to extract detailed content
|
||||||
|
- Synthesize information from multiple sources into clear, accurate answers
|
||||||
|
- Cite your sources inline so the user can verify information
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
- Always search the web before answering questions that require current or factual information
|
||||||
|
- Use multiple searches when needed to get comprehensive answers
|
||||||
|
- Cross-reference sources for accuracy
|
||||||
|
- Structure your responses clearly with headings, bullet points, and citations where appropriate
|
||||||
|
- When fetching pages, extract only the relevant information
|
||||||
|
- Be transparent about the sources you used
|
||||||
|
- Today's date is ${today}`
|
||||||
|
: `You are a helpful AI research assistant. Answer clearly and accurately. You do not have live web access, so be transparent when a question requires current information. Today's date is ${today}.`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
||||||
|
import {
|
||||||
|
DEFAULT_OPENROUTER_MODEL_ID,
|
||||||
|
isOpenRouterModelId,
|
||||||
|
type OpenRouterModelId,
|
||||||
|
} from '@/lib/openrouter';
|
||||||
|
|
||||||
|
export function getDefaultChatModelId(): OpenRouterModelId {
|
||||||
|
const configuredModel = process.env.OPENROUTER_MODEL;
|
||||||
|
return isOpenRouterModelId(configuredModel) ? configuredModel : DEFAULT_OPENROUTER_MODEL_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChatModel(modelId: OpenRouterModelId = getDefaultChatModelId()) {
|
||||||
|
const openrouter = createOpenRouter({
|
||||||
|
apiKey: process.env.OPENROUTER_API_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
return openrouter(modelId);
|
||||||
|
}
|
||||||
@@ -7,10 +7,11 @@ export const firecrawlWebFetch = tool({
|
|||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
url: z.string().url().describe('The URL of the web page to fetch'),
|
url: z.string().url().describe('The URL of the web page to fetch'),
|
||||||
}),
|
}),
|
||||||
execute: async ({ url }) => {
|
execute: async ({ url }, { abortSignal }) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('https://api.firecrawl.dev/v1/scrape', {
|
const response = await fetch('https://api.firecrawl.dev/v1/scrape', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
signal: abortSignal,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${process.env.FIRECRAWL_API_KEY!}`,
|
Authorization: `Bearer ${process.env.FIRECRAWL_API_KEY!}`,
|
||||||
@@ -13,10 +13,11 @@ export const firecrawlWebSearch = tool({
|
|||||||
.default(3)
|
.default(3)
|
||||||
.describe('Number of results to return (max 10)'),
|
.describe('Number of results to return (max 10)'),
|
||||||
}),
|
}),
|
||||||
execute: async ({ query, limit }) => {
|
execute: async ({ query, limit }, { abortSignal }) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('https://api.firecrawl.dev/v1/search', {
|
const response = await fetch('https://api.firecrawl.dev/v1/search', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
signal: abortSignal,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${process.env.FIRECRAWL_API_KEY!}`,
|
Authorization: `Bearer ${process.env.FIRECRAWL_API_KEY!}`,
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { firecrawlWebFetch } from './firecrawl-web-fetch';
|
||||||
|
import { firecrawlWebSearch } from './firecrawl-web-search';
|
||||||
|
|
||||||
|
// Keep these keys stable: saved conversations reference tool names.
|
||||||
|
export const chatTools = {
|
||||||
|
firecrawlWebSearch,
|
||||||
|
firecrawlWebFetch,
|
||||||
|
};
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { setTimeout as delay } from 'node:timers/promises';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import { Chat } from '@ai-sdk/react';
|
||||||
|
import { DefaultChatTransport } from 'ai';
|
||||||
|
import { createJiti } from 'jiti';
|
||||||
|
|
||||||
|
const jiti = createJiti(import.meta.url, {
|
||||||
|
alias: { '@': fileURLToPath(new URL('../', import.meta.url)) },
|
||||||
|
jsx: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function waitFor(predicate) {
|
||||||
|
const deadline = Date.now() + 3000;
|
||||||
|
while (!predicate()) {
|
||||||
|
assert.ok(Date.now() < deadline, 'Expected streaming update before timeout');
|
||||||
|
await delay(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function textOf(chat) {
|
||||||
|
return chat.messages.at(-1)?.parts
|
||||||
|
.filter((part) => part.type === 'text')
|
||||||
|
.map((part) => part.text)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setup(t) {
|
||||||
|
const environment = {
|
||||||
|
OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY,
|
||||||
|
FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY,
|
||||||
|
OPENROUTER_MODEL: process.env.OPENROUTER_MODEL,
|
||||||
|
};
|
||||||
|
process.env.OPENROUTER_API_KEY = 'streaming-test-key';
|
||||||
|
delete process.env.FIRECRAWL_API_KEY;
|
||||||
|
t.after(() => {
|
||||||
|
for (const [key, value] of Object.entries(environment)) {
|
||||||
|
if (value === undefined) delete process.env[key];
|
||||||
|
else process.env[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let controller;
|
||||||
|
let upstreamSignal;
|
||||||
|
let response;
|
||||||
|
let upstreamRequests = 0;
|
||||||
|
let upstreamModel;
|
||||||
|
let closed = false;
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const { POST } = await jiti.import('../app/api/chat/route.ts');
|
||||||
|
|
||||||
|
// Exercise the real route, OpenRouter adapter, and client transport. Only the
|
||||||
|
// external provider is replaced, so no credentials or network are required.
|
||||||
|
t.mock.method(globalThis, 'fetch', async (url, init) => {
|
||||||
|
assert.equal(String(url), 'https://openrouter.ai/api/v1/chat/completions');
|
||||||
|
assert.equal(JSON.parse(init.body).stream, true);
|
||||||
|
upstreamModel = JSON.parse(init.body).model;
|
||||||
|
upstreamRequests += 1;
|
||||||
|
upstreamSignal = init.signal;
|
||||||
|
return new Response(new ReadableStream({
|
||||||
|
start(value) {
|
||||||
|
controller = value;
|
||||||
|
upstreamSignal?.addEventListener('abort', () => {
|
||||||
|
if (!closed) {
|
||||||
|
closed = true;
|
||||||
|
controller.error(new DOMException('Aborted', 'AbortError'));
|
||||||
|
}
|
||||||
|
}, { once: true });
|
||||||
|
},
|
||||||
|
cancel() { closed = true; },
|
||||||
|
}), { headers: { 'Content-Type': 'text/event-stream' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
const chat = new Chat({
|
||||||
|
transport: new DefaultChatTransport({
|
||||||
|
api: 'http://localhost/api/chat',
|
||||||
|
fetch: async (url, init) => {
|
||||||
|
response = await POST(new Request(url, init));
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
t.after(async () => {
|
||||||
|
await chat.stop();
|
||||||
|
if (controller && !closed) {
|
||||||
|
closed = true;
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function event(data) {
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function chunk(delta, finishReason = null) {
|
||||||
|
event({
|
||||||
|
id: 'test-generation',
|
||||||
|
object: 'chat.completion.chunk',
|
||||||
|
created: 1,
|
||||||
|
model: 'test-model',
|
||||||
|
choices: [{ index: 0, delta, finish_reason: finishReason }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat,
|
||||||
|
get response() { return response; },
|
||||||
|
get upstreamRequests() { return upstreamRequests; },
|
||||||
|
get upstreamModel() { return upstreamModel; },
|
||||||
|
get upstreamSignal() { return upstreamSignal; },
|
||||||
|
ready: () => waitFor(() => Boolean(controller)),
|
||||||
|
chunk,
|
||||||
|
finish() {
|
||||||
|
chunk({}, 'stop');
|
||||||
|
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
||||||
|
closed = true;
|
||||||
|
controller.close();
|
||||||
|
},
|
||||||
|
fail() {
|
||||||
|
event({ error: { code: 502, message: 'Provider disconnected' } });
|
||||||
|
closed = true;
|
||||||
|
controller.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('streams reasoning and text to the client before the provider finishes', async (t) => {
|
||||||
|
const stream = await setup(t);
|
||||||
|
let completed = false;
|
||||||
|
const sending = stream.chat.sendMessage({ text: 'Hello' }).then(() => {
|
||||||
|
completed = true;
|
||||||
|
});
|
||||||
|
await stream.ready();
|
||||||
|
|
||||||
|
assert.match(stream.response.headers.get('content-type'), /text\/event-stream/);
|
||||||
|
assert.match(stream.response.headers.get('cache-control'), /no-transform/);
|
||||||
|
assert.equal(stream.response.headers.get('x-accel-buffering'), 'no');
|
||||||
|
stream.chunk({ reasoning: 'Thinking through the answer.' });
|
||||||
|
await waitFor(() => stream.chat.messages.at(-1)?.parts.some(
|
||||||
|
(part) => part.type === 'reasoning' && part.text === 'Thinking through the answer.'
|
||||||
|
));
|
||||||
|
|
||||||
|
stream.chunk({ content: 'Hello' });
|
||||||
|
await waitFor(() => textOf(stream.chat) === 'Hello');
|
||||||
|
assert.equal(stream.chat.status, 'streaming');
|
||||||
|
assert.equal(completed, false);
|
||||||
|
|
||||||
|
stream.chunk({ content: ' world' });
|
||||||
|
await waitFor(() => textOf(stream.chat) === 'Hello world');
|
||||||
|
assert.equal(completed, false);
|
||||||
|
stream.finish();
|
||||||
|
await sending;
|
||||||
|
assert.equal(stream.chat.status, 'ready');
|
||||||
|
assert.equal(textOf(stream.chat), 'Hello world');
|
||||||
|
assert.equal(stream.upstreamRequests, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const withPartialText of [false, true]) {
|
||||||
|
test(`stop aborts the upstream request ${withPartialText ? 'after a text chunk' : 'before the first token'}`, async (t) => {
|
||||||
|
const stream = await setup(t);
|
||||||
|
const errors = t.mock.method(console, 'error', () => {});
|
||||||
|
const sending = stream.chat.sendMessage({ text: 'Write a long response' });
|
||||||
|
await stream.ready();
|
||||||
|
if (withPartialText) {
|
||||||
|
stream.chunk({ content: 'Partial answer' });
|
||||||
|
await waitFor(() => textOf(stream.chat) === 'Partial answer');
|
||||||
|
}
|
||||||
|
|
||||||
|
await stream.chat.stop();
|
||||||
|
await sending;
|
||||||
|
assert.equal(stream.upstreamSignal.aborted, true);
|
||||||
|
assert.equal(stream.chat.status, 'ready');
|
||||||
|
assert.equal(stream.chat.error, undefined);
|
||||||
|
if (withPartialText) assert.equal(textOf(stream.chat), 'Partial answer');
|
||||||
|
assert.equal(stream.upstreamRequests, 1);
|
||||||
|
assert.equal(errors.mock.callCount(), 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('a mid-stream provider error preserves partial text and reaches the client', async (t) => {
|
||||||
|
const stream = await setup(t);
|
||||||
|
t.mock.method(console, 'error', () => {});
|
||||||
|
const sending = stream.chat.sendMessage({ text: 'Hello' });
|
||||||
|
await stream.ready();
|
||||||
|
stream.chunk({ content: 'Partial answer' });
|
||||||
|
await waitFor(() => textOf(stream.chat) === 'Partial answer');
|
||||||
|
stream.fail();
|
||||||
|
await sending;
|
||||||
|
|
||||||
|
assert.equal(stream.chat.status, 'error');
|
||||||
|
assert.equal(stream.chat.error.message, 'The response was interrupted. Please try again.');
|
||||||
|
assert.equal(textOf(stream.chat), 'Partial answer');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing credentials return an error without contacting the provider', async (t) => {
|
||||||
|
const stream = await setup(t);
|
||||||
|
delete process.env.OPENROUTER_API_KEY;
|
||||||
|
await stream.chat.sendMessage({ text: 'Hello' });
|
||||||
|
assert.equal(stream.response.status, 503);
|
||||||
|
assert.equal(stream.chat.status, 'error');
|
||||||
|
assert.equal(stream.upstreamRequests, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changing the selected model routes each message to that OpenRouter model', async (t) => {
|
||||||
|
const stream = await setup(t);
|
||||||
|
|
||||||
|
for (const [index, modelId] of ['anthropic/claude-opus-5', 'google/gemini-3.8-flash'].entries()) {
|
||||||
|
const sending = stream.chat.sendMessage({ text: `Message ${index + 1}` }, { body: { modelId } });
|
||||||
|
await waitFor(() => stream.upstreamRequests === index + 1);
|
||||||
|
assert.equal(stream.upstreamModel, modelId);
|
||||||
|
stream.chunk({ content: 'Response from the selected model.' });
|
||||||
|
stream.finish();
|
||||||
|
await sending;
|
||||||
|
assert.equal(stream.chat.status, 'ready');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requests without a selection use the configured catalog model', async (t) => {
|
||||||
|
const stream = await setup(t);
|
||||||
|
process.env.OPENROUTER_MODEL = 'deepseek/deepseek-v4-flash-0731';
|
||||||
|
const sending = stream.chat.sendMessage({ text: 'Hello' });
|
||||||
|
await stream.ready();
|
||||||
|
assert.equal(stream.upstreamModel, 'deepseek/deepseek-v4-flash-0731');
|
||||||
|
stream.finish();
|
||||||
|
await sending;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const modelId of ['unknown/model', null, { id: 'anthropic/claude-opus-5' }]) {
|
||||||
|
test(`rejects an invalid model selection (${JSON.stringify(modelId)}) without contacting OpenRouter`, async (t) => {
|
||||||
|
const stream = await setup(t);
|
||||||
|
await stream.chat.sendMessage({ text: 'Hello' }, { body: { modelId } });
|
||||||
|
assert.equal(stream.response.status, 400);
|
||||||
|
assert.equal(stream.chat.status, 'error');
|
||||||
|
assert.equal(stream.upstreamRequests, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -32,18 +32,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/openai@npm:^4.0.9":
|
|
||||||
version: 4.0.66
|
|
||||||
resolution: "@ai-sdk/openai@npm:4.0.66"
|
|
||||||
dependencies:
|
|
||||||
"@ai-sdk/provider": "npm:4.0.14"
|
|
||||||
"@ai-sdk/provider-utils": "npm:5.0.40"
|
|
||||||
peerDependencies:
|
|
||||||
zod: ^3.25.76 || ^4.1.8
|
|
||||||
checksum: 10c0/ed38f484cf9fb627f7ff9e77d7f5fc7071d6f1582b03a768d50306398de40157d79d0727c45f1a792b6989b970ea38df1cbc46cb96a0636c0c3e58794f4c59a4
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@ai-sdk/provider-utils@npm:5.0.40":
|
"@ai-sdk/provider-utils@npm:5.0.40":
|
||||||
version: 5.0.40
|
version: 5.0.40
|
||||||
resolution: "@ai-sdk/provider-utils@npm:5.0.40"
|
resolution: "@ai-sdk/provider-utils@npm:5.0.40"
|
||||||
@@ -487,19 +475,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@mendable/firecrawl-js@npm:^4.12.1":
|
|
||||||
version: 4.39.0
|
|
||||||
resolution: "@mendable/firecrawl-js@npm:4.39.0"
|
|
||||||
dependencies:
|
|
||||||
axios: "npm:1.18.0"
|
|
||||||
firecrawl: "npm:4.30.1"
|
|
||||||
typescript-event-target: "npm:^1.1.1"
|
|
||||||
zod: "npm:^3.23.8"
|
|
||||||
zod-to-json-schema: "npm:^3.23.0"
|
|
||||||
checksum: 10c0/88262fb0bc66406ea1c04b57f216116dff358179a7a1f85963e26bbbe87dbe3eb1451b63d62a9ff617a7bea0c41908ccf51d44ae6a62f44edf3fe9a6fc6d0f9d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime@npm:^1.1.4":
|
"@napi-rs/wasm-runtime@npm:^1.1.4":
|
||||||
version: 1.2.4
|
version: 1.2.4
|
||||||
resolution: "@napi-rs/wasm-runtime@npm:1.2.4"
|
resolution: "@napi-rs/wasm-runtime@npm:1.2.4"
|
||||||
@@ -512,13 +487,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@neondatabase/serverless@npm:^1.0.2":
|
|
||||||
version: 1.1.0
|
|
||||||
resolution: "@neondatabase/serverless@npm:1.1.0"
|
|
||||||
checksum: 10c0/ba24391bd53f94b76a817c9bfd09f91259024119b51e64192f2f97f627253d4370699e051d73918352e71fb93ae4575177ca1f5b38f2c27fcf5dc1adda70c677
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@next/env@npm:16.2.10":
|
"@next/env@npm:16.2.10":
|
||||||
version: 16.2.10
|
version: 16.2.10
|
||||||
resolution: "@next/env@npm:16.2.10"
|
resolution: "@next/env@npm:16.2.10"
|
||||||
@@ -1751,7 +1719,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-use-controllable-state@npm:1.2.6":
|
"@radix-ui/react-use-controllable-state@npm:1.2.6, @radix-ui/react-use-controllable-state@npm:^1.2.6":
|
||||||
version: 1.2.6
|
version: 1.2.6
|
||||||
resolution: "@radix-ui/react-use-controllable-state@npm:1.2.6"
|
resolution: "@radix-ui/react-use-controllable-state@npm:1.2.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2500,15 +2468,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"agent-base@npm:6":
|
|
||||||
version: 6.0.2
|
|
||||||
resolution: "agent-base@npm:6.0.2"
|
|
||||||
dependencies:
|
|
||||||
debug: "npm:4"
|
|
||||||
checksum: 10c0/dc4f757e40b5f3e3d674bc9beb4f1048f4ee83af189bae39be99f57bf1f48dde166a8b0a5342a84b5944ee8e6ed1e5a9d801858f4ad44764e84957122fe46261
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"ai@npm:7.0.99, ai@npm:^7.0.18":
|
"ai@npm:7.0.99, ai@npm:^7.0.18":
|
||||||
version: 7.0.99
|
version: 7.0.99
|
||||||
resolution: "ai@npm:7.0.99"
|
resolution: "ai@npm:7.0.99"
|
||||||
@@ -2531,39 +2490,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"async-function@npm:^1.0.0":
|
|
||||||
version: 1.0.0
|
|
||||||
resolution: "async-function@npm:1.0.0"
|
|
||||||
checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"async-generator-function@npm:^1.0.0":
|
|
||||||
version: 1.0.0
|
|
||||||
resolution: "async-generator-function@npm:1.0.0"
|
|
||||||
checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"asynckit@npm:^0.4.0":
|
|
||||||
version: 0.4.0
|
|
||||||
resolution: "asynckit@npm:0.4.0"
|
|
||||||
checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"axios@npm:1.18.0":
|
|
||||||
version: 1.18.0
|
|
||||||
resolution: "axios@npm:1.18.0"
|
|
||||||
dependencies:
|
|
||||||
follow-redirects: "npm:^1.16.0"
|
|
||||||
form-data: "npm:^4.0.5"
|
|
||||||
https-proxy-agent: "npm:^5.0.1"
|
|
||||||
proxy-from-env: "npm:^2.1.0"
|
|
||||||
checksum: 10c0/f88aae13fd9dd395dea3053c7299178fee62f436f20424a1d6d79ec11ffb7656ba664b4630defbadab2c387b6ea07e29afb9f28b26d8e0e49d45be766983290d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"bail@npm:^2.0.0":
|
"bail@npm:^2.0.0":
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
resolution: "bail@npm:2.0.2"
|
resolution: "bail@npm:2.0.2"
|
||||||
@@ -2580,16 +2506,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2":
|
|
||||||
version: 1.0.2
|
|
||||||
resolution: "call-bind-apply-helpers@npm:1.0.2"
|
|
||||||
dependencies:
|
|
||||||
es-errors: "npm:^1.3.0"
|
|
||||||
function-bind: "npm:^1.1.2"
|
|
||||||
checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"caniuse-lite@npm:^1.0.30001579":
|
"caniuse-lite@npm:^1.0.30001579":
|
||||||
version: 1.0.30001810
|
version: 1.0.30001810
|
||||||
resolution: "caniuse-lite@npm:1.0.30001810"
|
resolution: "caniuse-lite@npm:1.0.30001810"
|
||||||
@@ -2677,15 +2593,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"combined-stream@npm:^1.0.8":
|
|
||||||
version: 1.0.8
|
|
||||||
resolution: "combined-stream@npm:1.0.8"
|
|
||||||
dependencies:
|
|
||||||
delayed-stream: "npm:~1.0.0"
|
|
||||||
checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"comma-separated-tokens@npm:^2.0.0":
|
"comma-separated-tokens@npm:^2.0.0":
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
resolution: "comma-separated-tokens@npm:2.0.3"
|
resolution: "comma-separated-tokens@npm:2.0.3"
|
||||||
@@ -2693,15 +2600,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"cross-fetch@npm:~4.1.0":
|
|
||||||
version: 4.1.0
|
|
||||||
resolution: "cross-fetch@npm:4.1.0"
|
|
||||||
dependencies:
|
|
||||||
node-fetch: "npm:^2.7.0"
|
|
||||||
checksum: 10c0/628b134ea27cfcada67025afe6ef1419813fffc5d63d175553efa75a2334522d450300a0f3f0719029700da80e96327930709d5551cf6deb39bb62f1d536642e
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"cross-spawn@npm:^7.0.6":
|
"cross-spawn@npm:^7.0.6":
|
||||||
version: 7.0.6
|
version: 7.0.6
|
||||||
resolution: "cross-spawn@npm:7.0.6"
|
resolution: "cross-spawn@npm:7.0.6"
|
||||||
@@ -2879,7 +2777,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"debug@npm:4, debug@npm:^4.0.0":
|
"debug@npm:^4.0.0":
|
||||||
version: 4.4.3
|
version: 4.4.3
|
||||||
resolution: "debug@npm:4.4.3"
|
resolution: "debug@npm:4.4.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2907,13 +2805,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"delayed-stream@npm:~1.0.0":
|
|
||||||
version: 1.0.0
|
|
||||||
resolution: "delayed-stream@npm:1.0.0"
|
|
||||||
checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"dequal@npm:^2.0.0, dequal@npm:^2.0.3":
|
"dequal@npm:^2.0.0, dequal@npm:^2.0.3":
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
resolution: "dequal@npm:2.0.3"
|
resolution: "dequal@npm:2.0.3"
|
||||||
@@ -2944,119 +2835,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"dotenv@npm:~16.4.7":
|
|
||||||
version: 16.4.7
|
|
||||||
resolution: "dotenv@npm:16.4.7"
|
|
||||||
checksum: 10c0/be9f597e36a8daf834452daa1f4cc30e5375a5968f98f46d89b16b983c567398a330580c88395069a77473943c06b877d1ca25b4afafcdd6d4adb549e8293462
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"drizzle-orm@npm:^0.45.1":
|
|
||||||
version: 0.45.2
|
|
||||||
resolution: "drizzle-orm@npm:0.45.2"
|
|
||||||
peerDependencies:
|
|
||||||
"@aws-sdk/client-rds-data": ">=3"
|
|
||||||
"@cloudflare/workers-types": ">=4"
|
|
||||||
"@electric-sql/pglite": ">=0.2.0"
|
|
||||||
"@libsql/client": ">=0.10.0"
|
|
||||||
"@libsql/client-wasm": ">=0.10.0"
|
|
||||||
"@neondatabase/serverless": ">=0.10.0"
|
|
||||||
"@op-engineering/op-sqlite": ">=2"
|
|
||||||
"@opentelemetry/api": ^1.4.1
|
|
||||||
"@planetscale/database": ">=1.13"
|
|
||||||
"@prisma/client": "*"
|
|
||||||
"@tidbcloud/serverless": "*"
|
|
||||||
"@types/better-sqlite3": "*"
|
|
||||||
"@types/pg": "*"
|
|
||||||
"@types/sql.js": "*"
|
|
||||||
"@upstash/redis": ">=1.34.7"
|
|
||||||
"@vercel/postgres": ">=0.8.0"
|
|
||||||
"@xata.io/client": "*"
|
|
||||||
better-sqlite3: ">=7"
|
|
||||||
bun-types: "*"
|
|
||||||
expo-sqlite: ">=14.0.0"
|
|
||||||
gel: ">=2"
|
|
||||||
knex: "*"
|
|
||||||
kysely: "*"
|
|
||||||
mysql2: ">=2"
|
|
||||||
pg: ">=8"
|
|
||||||
postgres: ">=3"
|
|
||||||
sql.js: ">=1"
|
|
||||||
sqlite3: ">=5"
|
|
||||||
peerDependenciesMeta:
|
|
||||||
"@aws-sdk/client-rds-data":
|
|
||||||
optional: true
|
|
||||||
"@cloudflare/workers-types":
|
|
||||||
optional: true
|
|
||||||
"@electric-sql/pglite":
|
|
||||||
optional: true
|
|
||||||
"@libsql/client":
|
|
||||||
optional: true
|
|
||||||
"@libsql/client-wasm":
|
|
||||||
optional: true
|
|
||||||
"@neondatabase/serverless":
|
|
||||||
optional: true
|
|
||||||
"@op-engineering/op-sqlite":
|
|
||||||
optional: true
|
|
||||||
"@opentelemetry/api":
|
|
||||||
optional: true
|
|
||||||
"@planetscale/database":
|
|
||||||
optional: true
|
|
||||||
"@prisma/client":
|
|
||||||
optional: true
|
|
||||||
"@tidbcloud/serverless":
|
|
||||||
optional: true
|
|
||||||
"@types/better-sqlite3":
|
|
||||||
optional: true
|
|
||||||
"@types/pg":
|
|
||||||
optional: true
|
|
||||||
"@types/sql.js":
|
|
||||||
optional: true
|
|
||||||
"@upstash/redis":
|
|
||||||
optional: true
|
|
||||||
"@vercel/postgres":
|
|
||||||
optional: true
|
|
||||||
"@xata.io/client":
|
|
||||||
optional: true
|
|
||||||
better-sqlite3:
|
|
||||||
optional: true
|
|
||||||
bun-types:
|
|
||||||
optional: true
|
|
||||||
expo-sqlite:
|
|
||||||
optional: true
|
|
||||||
gel:
|
|
||||||
optional: true
|
|
||||||
knex:
|
|
||||||
optional: true
|
|
||||||
kysely:
|
|
||||||
optional: true
|
|
||||||
mysql2:
|
|
||||||
optional: true
|
|
||||||
pg:
|
|
||||||
optional: true
|
|
||||||
postgres:
|
|
||||||
optional: true
|
|
||||||
prisma:
|
|
||||||
optional: true
|
|
||||||
sql.js:
|
|
||||||
optional: true
|
|
||||||
sqlite3:
|
|
||||||
optional: true
|
|
||||||
checksum: 10c0/deb4a0230a79b1ff8c61365808bdb06a78e9e32171625f1c61d791dec445fad5cd9bc4ce2e6175b0ff1be7fb913310b2fd07b94a2cbe8027581b7d99469b8060
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"dunder-proto@npm:^1.0.1":
|
|
||||||
version: 1.0.1
|
|
||||||
resolution: "dunder-proto@npm:1.0.1"
|
|
||||||
dependencies:
|
|
||||||
call-bind-apply-helpers: "npm:^1.0.1"
|
|
||||||
es-errors: "npm:^1.3.0"
|
|
||||||
gopd: "npm:^1.2.0"
|
|
||||||
checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"embla-carousel-react@npm:^8.5.2":
|
"embla-carousel-react@npm:^8.5.2":
|
||||||
version: 8.6.0
|
version: 8.6.0
|
||||||
resolution: "embla-carousel-react@npm:8.6.0"
|
resolution: "embla-carousel-react@npm:8.6.0"
|
||||||
@@ -3102,41 +2880,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"es-define-property@npm:^1.0.1":
|
|
||||||
version: 1.0.1
|
|
||||||
resolution: "es-define-property@npm:1.0.1"
|
|
||||||
checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"es-errors@npm:^1.3.0":
|
|
||||||
version: 1.3.0
|
|
||||||
resolution: "es-errors@npm:1.3.0"
|
|
||||||
checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1":
|
|
||||||
version: 1.1.2
|
|
||||||
resolution: "es-object-atoms@npm:1.1.2"
|
|
||||||
dependencies:
|
|
||||||
es-errors: "npm:^1.3.0"
|
|
||||||
checksum: 10c0/1772861f094f739d6f41b579cfb9a18579daffeb434552a370a5fbef50a32d22227e27b63fdbb757b7ddd429d1b42fe52ccae7966d9302a2ec221b6f1b41bbc4
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"es-set-tostringtag@npm:^2.1.0":
|
|
||||||
version: 2.1.0
|
|
||||||
resolution: "es-set-tostringtag@npm:2.1.0"
|
|
||||||
dependencies:
|
|
||||||
es-errors: "npm:^1.3.0"
|
|
||||||
get-intrinsic: "npm:^1.2.6"
|
|
||||||
has-tostringtag: "npm:^1.0.2"
|
|
||||||
hasown: "npm:^2.0.2"
|
|
||||||
checksum: 10c0/ef2ca9ce49afe3931cb32e35da4dcb6d86ab02592cfc2ce3e49ced199d9d0bb5085fc7e73e06312213765f5efa47cc1df553a6a5154584b21448e9fb8355b1af
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"es-toolkit@npm:^1.39.3":
|
"es-toolkit@npm:^1.39.3":
|
||||||
version: 1.52.0
|
version: 1.52.0
|
||||||
resolution: "es-toolkit@npm:1.52.0"
|
resolution: "es-toolkit@npm:1.52.0"
|
||||||
@@ -3179,19 +2922,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"exa-js@npm:^2.4.0":
|
|
||||||
version: 2.19.0
|
|
||||||
resolution: "exa-js@npm:2.19.0"
|
|
||||||
dependencies:
|
|
||||||
cross-fetch: "npm:~4.1.0"
|
|
||||||
dotenv: "npm:~16.4.7"
|
|
||||||
openai: "npm:^5.0.1"
|
|
||||||
zod: "npm:^3.22.0"
|
|
||||||
zod-to-json-schema: "npm:^3.20.0"
|
|
||||||
checksum: 10c0/baaf37a4a1d3fffbe162b5dcff3df9f09d8a3597cf4016e43d10fcc4bb90e810ab22bda92a3144fa235fefeb2ad83bbd5746a89487a9d755ac6205ebd6a1d7f4
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"extend@npm:^3.0.0":
|
"extend@npm:^3.0.0":
|
||||||
version: 3.0.2
|
version: 3.0.2
|
||||||
resolution: "extend@npm:3.0.2"
|
resolution: "extend@npm:3.0.2"
|
||||||
@@ -3199,98 +2929,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"firecrawl@npm:4.30.1":
|
|
||||||
version: 4.30.1
|
|
||||||
resolution: "firecrawl@npm:4.30.1"
|
|
||||||
dependencies:
|
|
||||||
axios: "npm:1.18.0"
|
|
||||||
typescript-event-target: "npm:^1.1.1"
|
|
||||||
zod: "npm:^3.23.8"
|
|
||||||
zod-to-json-schema: "npm:^3.23.0"
|
|
||||||
checksum: 10c0/52dfc34cfb4363e7217ff57171308069689ef2de3d894d191d221964b05cfbb1410b2d1bd407f9875f865e9471a6580085b956d02a4cc38a28bdce79244eaeb4
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"follow-redirects@npm:^1.16.0":
|
|
||||||
version: 1.16.0
|
|
||||||
resolution: "follow-redirects@npm:1.16.0"
|
|
||||||
peerDependenciesMeta:
|
|
||||||
debug:
|
|
||||||
optional: true
|
|
||||||
checksum: 10c0/a1e2900163e6f1b4d1ed5c221b607f41decbab65534c63fe7e287e40a5d552a6496e7d9d7d976fa4ba77b4c51c11e5e9f683f10b43011ea11e442ff128d0e181
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"form-data@npm:^4.0.5":
|
|
||||||
version: 4.0.6
|
|
||||||
resolution: "form-data@npm:4.0.6"
|
|
||||||
dependencies:
|
|
||||||
asynckit: "npm:^0.4.0"
|
|
||||||
combined-stream: "npm:^1.0.8"
|
|
||||||
es-set-tostringtag: "npm:^2.1.0"
|
|
||||||
hasown: "npm:^2.0.4"
|
|
||||||
mime-types: "npm:^2.1.35"
|
|
||||||
checksum: 10c0/43947a77bf0ff45c6ceed789778982d47a3f3e720a74b71721174ebf3310a5f1a8be1d6b38a3ee3688e8a18a2c4273073ec0844cd37efda3eaf46d41c9c318ff
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"framer-motion@npm:^12.0.0, framer-motion@npm:^12.43.0":
|
|
||||||
version: 12.43.0
|
|
||||||
resolution: "framer-motion@npm:12.43.0"
|
|
||||||
dependencies:
|
|
||||||
motion-dom: "npm:^12.43.0"
|
|
||||||
motion-utils: "npm:^12.39.0"
|
|
||||||
tslib: "npm:^2.4.0"
|
|
||||||
peerDependencies:
|
|
||||||
"@emotion/is-prop-valid": "*"
|
|
||||||
react: ^18.0.0 || ^19.0.0
|
|
||||||
react-dom: ^18.0.0 || ^19.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
"@emotion/is-prop-valid":
|
|
||||||
optional: true
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
react-dom:
|
|
||||||
optional: true
|
|
||||||
checksum: 10c0/cce3c1daf8f53da4d9299e6bdd2a1e9f08a30e13d84ce1e468cf25285f9ec2a900c812e0aa3dce79e7453a2a3cf328a717451beef9bf0a6175a3d3794116e36a
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"function-bind@npm:^1.1.2":
|
|
||||||
version: 1.1.2
|
|
||||||
resolution: "function-bind@npm:1.1.2"
|
|
||||||
checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"generator-function@npm:^2.0.0":
|
|
||||||
version: 2.0.1
|
|
||||||
resolution: "generator-function@npm:2.0.1"
|
|
||||||
checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"get-intrinsic@npm:^1.2.6":
|
|
||||||
version: 1.3.1
|
|
||||||
resolution: "get-intrinsic@npm:1.3.1"
|
|
||||||
dependencies:
|
|
||||||
async-function: "npm:^1.0.0"
|
|
||||||
async-generator-function: "npm:^1.0.0"
|
|
||||||
call-bind-apply-helpers: "npm:^1.0.2"
|
|
||||||
es-define-property: "npm:^1.0.1"
|
|
||||||
es-errors: "npm:^1.3.0"
|
|
||||||
es-object-atoms: "npm:^1.1.1"
|
|
||||||
function-bind: "npm:^1.1.2"
|
|
||||||
generator-function: "npm:^2.0.0"
|
|
||||||
get-proto: "npm:^1.0.1"
|
|
||||||
gopd: "npm:^1.2.0"
|
|
||||||
has-symbols: "npm:^1.1.0"
|
|
||||||
hasown: "npm:^2.0.2"
|
|
||||||
math-intrinsics: "npm:^1.1.0"
|
|
||||||
checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"get-nonce@npm:^1.0.0":
|
"get-nonce@npm:^1.0.0":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "get-nonce@npm:1.0.1"
|
resolution: "get-nonce@npm:1.0.1"
|
||||||
@@ -3298,30 +2936,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"get-proto@npm:^1.0.1":
|
|
||||||
version: 1.0.1
|
|
||||||
resolution: "get-proto@npm:1.0.1"
|
|
||||||
dependencies:
|
|
||||||
dunder-proto: "npm:^1.0.1"
|
|
||||||
es-object-atoms: "npm:^1.0.0"
|
|
||||||
checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"gopd@npm:^1.2.0":
|
|
||||||
version: 1.2.0
|
|
||||||
resolution: "gopd@npm:1.2.0"
|
|
||||||
checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"gql-tag@npm:^1.0.1":
|
|
||||||
version: 1.0.1
|
|
||||||
resolution: "gql-tag@npm:1.0.1"
|
|
||||||
checksum: 10c0/6acf28755f0d2143d1f76d75955b48369386c38ae0928f2b6cc8e1ea1ff5d0e58adc718e9a4ce0831f91a7761ece6c1a46f5817b2af0dbd4289960c0a4073eeb
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"graceful-fs@npm:^4.2.4":
|
"graceful-fs@npm:^4.2.4":
|
||||||
version: 4.2.11
|
version: 4.2.11
|
||||||
resolution: "graceful-fs@npm:4.2.11"
|
resolution: "graceful-fs@npm:4.2.11"
|
||||||
@@ -3329,31 +2943,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0":
|
|
||||||
version: 1.1.0
|
|
||||||
resolution: "has-symbols@npm:1.1.0"
|
|
||||||
checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"has-tostringtag@npm:^1.0.2":
|
|
||||||
version: 1.0.2
|
|
||||||
resolution: "has-tostringtag@npm:1.0.2"
|
|
||||||
dependencies:
|
|
||||||
has-symbols: "npm:^1.0.3"
|
|
||||||
checksum: 10c0/a8b166462192bafe3d9b6e420a1d581d93dd867adb61be223a17a8d6dad147aa77a8be32c961bb2f27b3ef893cae8d36f564ab651f5e9b7938ae86f74027c48c
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"hasown@npm:^2.0.2, hasown@npm:^2.0.4":
|
|
||||||
version: 2.0.4
|
|
||||||
resolution: "hasown@npm:2.0.4"
|
|
||||||
dependencies:
|
|
||||||
function-bind: "npm:^1.1.2"
|
|
||||||
checksum: 10c0/2d8de939e270b70618f8cebb69746620db10617dbb495bc66ddad326955ea24d3ca4af133aff3eb7c1853e0218f867bc2b050ec26fe02e3aea58f880ffc5e506
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"hast-util-from-parse5@npm:^8.0.0":
|
"hast-util-from-parse5@npm:^8.0.0":
|
||||||
version: 8.0.3
|
version: 8.0.3
|
||||||
resolution: "hast-util-from-parse5@npm:8.0.3"
|
resolution: "hast-util-from-parse5@npm:8.0.3"
|
||||||
@@ -3430,7 +3019,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"hast-util-to-jsx-runtime@npm:^2.0.0, hast-util-to-jsx-runtime@npm:^2.3.6":
|
"hast-util-to-jsx-runtime@npm:^2.3.6":
|
||||||
version: 2.3.6
|
version: 2.3.6
|
||||||
resolution: "hast-util-to-jsx-runtime@npm:2.3.6"
|
resolution: "hast-util-to-jsx-runtime@npm:2.3.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3490,7 +3079,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"html-url-attributes@npm:^3.0.0, html-url-attributes@npm:^3.0.1":
|
"html-url-attributes@npm:^3.0.1":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "html-url-attributes@npm:3.0.1"
|
resolution: "html-url-attributes@npm:3.0.1"
|
||||||
checksum: 10c0/496e4908aa8b77665f348b4b03521901794f648b8ac34a581022cd6f2c97934d5c910cd91bc6593bbf2994687549037bc2520fcdc769b31484f29ffdd402acd0
|
checksum: 10c0/496e4908aa8b77665f348b4b03521901794f648b8ac34a581022cd6f2c97934d5c910cd91bc6593bbf2994687549037bc2520fcdc769b31484f29ffdd402acd0
|
||||||
@@ -3504,16 +3093,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"https-proxy-agent@npm:^5.0.1":
|
|
||||||
version: 5.0.1
|
|
||||||
resolution: "https-proxy-agent@npm:5.0.1"
|
|
||||||
dependencies:
|
|
||||||
agent-base: "npm:6"
|
|
||||||
debug: "npm:4"
|
|
||||||
checksum: 10c0/6dd639f03434003577c62b27cafdb864784ef19b2de430d8ae2a1d45e31c4fd60719e5637b44db1a88a046934307da7089e03d6089ec3ddacc1189d8de8897d1
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"immer@npm:^11.0.0, immer@npm:^11.1.8":
|
"immer@npm:^11.0.0, immer@npm:^11.1.8":
|
||||||
version: 11.1.18
|
version: 11.1.18
|
||||||
resolution: "immer@npm:11.1.18"
|
resolution: "immer@npm:11.1.18"
|
||||||
@@ -3733,15 +3312,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"lucide-react@npm:^0.574.0":
|
|
||||||
version: 0.574.0
|
|
||||||
resolution: "lucide-react@npm:0.574.0"
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
checksum: 10c0/bfb9ed34520657faae130a00247dd35516ab77bf00eb1b2c6995bcbb6919de8edde3546f29c85b5cc6bd1c0f5dd7ea9147c7777f0d7e75208aee19997eb0f752
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"magic-string@npm:^0.30.21":
|
"magic-string@npm:^0.30.21":
|
||||||
version: 0.30.21
|
version: 0.30.21
|
||||||
resolution: "magic-string@npm:0.30.21"
|
resolution: "magic-string@npm:0.30.21"
|
||||||
@@ -3767,13 +3337,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"math-intrinsics@npm:^1.1.0":
|
|
||||||
version: 1.1.0
|
|
||||||
resolution: "math-intrinsics@npm:1.1.0"
|
|
||||||
checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"mdast-util-find-and-replace@npm:^3.0.0":
|
"mdast-util-find-and-replace@npm:^3.0.0":
|
||||||
version: 3.0.2
|
version: 3.0.2
|
||||||
resolution: "mdast-util-find-and-replace@npm:3.0.2"
|
resolution: "mdast-util-find-and-replace@npm:3.0.2"
|
||||||
@@ -4313,59 +3876,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"mime-db@npm:1.52.0":
|
|
||||||
version: 1.52.0
|
|
||||||
resolution: "mime-db@npm:1.52.0"
|
|
||||||
checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"mime-types@npm:^2.1.35":
|
|
||||||
version: 2.1.35
|
|
||||||
resolution: "mime-types@npm:2.1.35"
|
|
||||||
dependencies:
|
|
||||||
mime-db: "npm:1.52.0"
|
|
||||||
checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"motion-dom@npm:^12.43.0":
|
|
||||||
version: 12.43.0
|
|
||||||
resolution: "motion-dom@npm:12.43.0"
|
|
||||||
dependencies:
|
|
||||||
motion-utils: "npm:^12.39.0"
|
|
||||||
checksum: 10c0/ef7d3f6eaa6cfeb1a939847a8b2581d119f2efc054948921ece0e2b1091f0969c15ca7707b87a46dddac7f156a714878f168e7b9f6ea189eeb7d5d9fbbb99d26
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"motion-utils@npm:^12.39.0":
|
|
||||||
version: 12.39.0
|
|
||||||
resolution: "motion-utils@npm:12.39.0"
|
|
||||||
checksum: 10c0/6d7a2a2cc0797b72410a666a9cc1c201c8e39bf9669670464e433fe1e72af5f0217154c869867b34fbadf3664cf222c0d022bbc4eed7927f201ae971918e7440
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"motion@npm:^12.33.0":
|
|
||||||
version: 12.43.0
|
|
||||||
resolution: "motion@npm:12.43.0"
|
|
||||||
dependencies:
|
|
||||||
framer-motion: "npm:^12.43.0"
|
|
||||||
tslib: "npm:^2.4.0"
|
|
||||||
peerDependencies:
|
|
||||||
"@emotion/is-prop-valid": "*"
|
|
||||||
react: ^18.0.0 || ^19.0.0
|
|
||||||
react-dom: ^18.0.0 || ^19.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
"@emotion/is-prop-valid":
|
|
||||||
optional: true
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
react-dom:
|
|
||||||
optional: true
|
|
||||||
checksum: 10c0/15544ecc71d356661a959c78c4f84b3ca91728f5d17265b68912d2bbcf86f20409ebf5acf2f6c82e0c0425d1024267b04abd5b786177a3694bf706f7a3a529d6
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"ms@npm:^2.1.3":
|
"ms@npm:^2.1.3":
|
||||||
version: 2.1.3
|
version: 2.1.3
|
||||||
resolution: "ms@npm:2.1.3"
|
resolution: "ms@npm:2.1.3"
|
||||||
@@ -4461,15 +3971,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"nextjs-app@workspace:.":
|
"nextjs-chatgpt-template@workspace:.":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "nextjs-app@workspace:."
|
resolution: "nextjs-chatgpt-template@workspace:."
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/openai": "npm:^4.0.9"
|
|
||||||
"@ai-sdk/react": "npm:^4.0.19"
|
"@ai-sdk/react": "npm:^4.0.19"
|
||||||
"@base-ui/react": "npm:^1.8.0"
|
"@base-ui/react": "npm:^1.8.0"
|
||||||
"@mendable/firecrawl-js": "npm:^4.12.1"
|
|
||||||
"@neondatabase/serverless": "npm:^1.0.2"
|
|
||||||
"@next/swc-wasm-web": "npm:16.2.10"
|
"@next/swc-wasm-web": "npm:16.2.10"
|
||||||
"@openrouter/ai-sdk-provider": "npm:^3.0.0"
|
"@openrouter/ai-sdk-provider": "npm:^3.0.0"
|
||||||
"@radix-ui/react-accordion": "npm:^1.2.3"
|
"@radix-ui/react-accordion": "npm:^1.2.3"
|
||||||
@@ -4499,6 +4006,7 @@ __metadata:
|
|||||||
"@radix-ui/react-toggle": "npm:^1.1.2"
|
"@radix-ui/react-toggle": "npm:^1.1.2"
|
||||||
"@radix-ui/react-toggle-group": "npm:^1.1.2"
|
"@radix-ui/react-toggle-group": "npm:^1.1.2"
|
||||||
"@radix-ui/react-tooltip": "npm:^1.1.8"
|
"@radix-ui/react-tooltip": "npm:^1.1.8"
|
||||||
|
"@radix-ui/react-use-controllable-state": "npm:^1.2.6"
|
||||||
"@remixicon/react": "npm:^4.9.0"
|
"@remixicon/react": "npm:^4.9.0"
|
||||||
"@tailwindcss/postcss": "npm:^4"
|
"@tailwindcss/postcss": "npm:^4"
|
||||||
"@types/node": "npm:^22"
|
"@types/node": "npm:^22"
|
||||||
@@ -4511,25 +4019,18 @@ __metadata:
|
|||||||
clsx: "npm:^2.1.1"
|
clsx: "npm:^2.1.1"
|
||||||
cmdk: "npm:^1.0.4"
|
cmdk: "npm:^1.0.4"
|
||||||
date-fns: "npm:^4.1.0"
|
date-fns: "npm:^4.1.0"
|
||||||
drizzle-orm: "npm:^0.45.1"
|
|
||||||
embla-carousel-react: "npm:^8.5.2"
|
embla-carousel-react: "npm:^8.5.2"
|
||||||
exa-js: "npm:^2.4.0"
|
|
||||||
framer-motion: "npm:^12.0.0"
|
|
||||||
gql-tag: "npm:^1.0.1"
|
|
||||||
input-otp: "npm:^1.4.2"
|
input-otp: "npm:^1.4.2"
|
||||||
lucide-react: "npm:^0.574.0"
|
jiti: "npm:^2.7.0"
|
||||||
motion: "npm:^12.33.0"
|
|
||||||
nanoid: "npm:^5.1.6"
|
nanoid: "npm:^5.1.6"
|
||||||
next: "npm:16.2.10"
|
next: "npm:16.2.10"
|
||||||
next-themes: "npm:^0.4.6"
|
next-themes: "npm:^0.4.6"
|
||||||
openai: "npm:^6.17.0"
|
|
||||||
radix-ui: "npm:^1.4.3"
|
radix-ui: "npm:^1.4.3"
|
||||||
react: "npm:^19.0.0"
|
react: "npm:^19.0.0"
|
||||||
react-day-picker: "npm:^9.5.0"
|
react-day-picker: "npm:^9.5.0"
|
||||||
react-dom: "npm:^19.0.0"
|
react-dom: "npm:^19.0.0"
|
||||||
react-hook-form: "npm:^7.88.0"
|
react-hook-form: "npm:^7.88.0"
|
||||||
react-is: "npm:^19.3.0"
|
react-is: "npm:^19.3.0"
|
||||||
react-markdown: "npm:^10.1.0"
|
|
||||||
react-resizable-panels: "npm:^4.5.8"
|
react-resizable-panels: "npm:^4.5.8"
|
||||||
recharts: "npm:^3.7.0"
|
recharts: "npm:^3.7.0"
|
||||||
shiki: "npm:^3.22.0"
|
shiki: "npm:^3.22.0"
|
||||||
@@ -4546,20 +4047,6 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"node-fetch@npm:^2.7.0":
|
|
||||||
version: 2.7.0
|
|
||||||
resolution: "node-fetch@npm:2.7.0"
|
|
||||||
dependencies:
|
|
||||||
whatwg-url: "npm:^5.0.0"
|
|
||||||
peerDependencies:
|
|
||||||
encoding: ^0.1.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
encoding:
|
|
||||||
optional: true
|
|
||||||
checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"oniguruma-parser@npm:^0.12.2":
|
"oniguruma-parser@npm:^0.12.2":
|
||||||
version: 0.12.2
|
version: 0.12.2
|
||||||
resolution: "oniguruma-parser@npm:0.12.2"
|
resolution: "oniguruma-parser@npm:0.12.2"
|
||||||
@@ -4578,47 +4065,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"openai@npm:^5.0.1":
|
|
||||||
version: 5.23.2
|
|
||||||
resolution: "openai@npm:5.23.2"
|
|
||||||
peerDependencies:
|
|
||||||
ws: ^8.18.0
|
|
||||||
zod: ^3.23.8
|
|
||||||
peerDependenciesMeta:
|
|
||||||
ws:
|
|
||||||
optional: true
|
|
||||||
zod:
|
|
||||||
optional: true
|
|
||||||
bin:
|
|
||||||
openai: bin/cli
|
|
||||||
checksum: 10c0/8ee37f37c64fd04a61622c4782d231e8f0245e2d85956d833e43d30946edbf07d5276cd74e09aa5d23006de8da43dad312246a9dd7ca6ca6d65d983fc976ffa6
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"openai@npm:^6.17.0":
|
|
||||||
version: 6.49.0
|
|
||||||
resolution: "openai@npm:6.49.0"
|
|
||||||
peerDependencies:
|
|
||||||
"@aws-sdk/credential-provider-node": ">=3.972.0 <4"
|
|
||||||
"@smithy/hash-node": ">=4.3.0 <5"
|
|
||||||
"@smithy/signature-v4": ">=5.4.0 <6"
|
|
||||||
ws: ^8.18.0
|
|
||||||
zod: ^3.25 || ^4.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
"@aws-sdk/credential-provider-node":
|
|
||||||
optional: true
|
|
||||||
"@smithy/hash-node":
|
|
||||||
optional: true
|
|
||||||
"@smithy/signature-v4":
|
|
||||||
optional: true
|
|
||||||
ws:
|
|
||||||
optional: true
|
|
||||||
zod:
|
|
||||||
optional: true
|
|
||||||
checksum: 10c0/c9e748e8a813f948ed925949e857d758505d246d7ec7e939f79cc2ab3383083ccdefff953479afb9dcb0f634cfddb0d442fcf02b6203cc9d6b7c95d3ddbc20ef
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"parse-entities@npm:^4.0.0":
|
"parse-entities@npm:^4.0.0":
|
||||||
version: 4.0.2
|
version: 4.0.2
|
||||||
resolution: "parse-entities@npm:4.0.2"
|
resolution: "parse-entities@npm:4.0.2"
|
||||||
@@ -4693,13 +4139,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"proxy-from-env@npm:^2.1.0":
|
|
||||||
version: 2.1.0
|
|
||||||
resolution: "proxy-from-env@npm:2.1.0"
|
|
||||||
checksum: 10c0/ed01729fd4d094eab619cd7e17ce3698b3413b31eb102c4904f9875e677cd207392795d5b4adee9cec359dfd31c44d5ad7595a3a3ad51c40250e141512281c58
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"radix-ui@npm:^1.4.3":
|
"radix-ui@npm:^1.4.3":
|
||||||
version: 1.6.7
|
version: 1.6.7
|
||||||
resolution: "radix-ui@npm:1.6.7"
|
resolution: "radix-ui@npm:1.6.7"
|
||||||
@@ -4814,28 +4253,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-markdown@npm:^10.1.0":
|
|
||||||
version: 10.1.0
|
|
||||||
resolution: "react-markdown@npm:10.1.0"
|
|
||||||
dependencies:
|
|
||||||
"@types/hast": "npm:^3.0.0"
|
|
||||||
"@types/mdast": "npm:^4.0.0"
|
|
||||||
devlop: "npm:^1.0.0"
|
|
||||||
hast-util-to-jsx-runtime: "npm:^2.0.0"
|
|
||||||
html-url-attributes: "npm:^3.0.0"
|
|
||||||
mdast-util-to-hast: "npm:^13.0.0"
|
|
||||||
remark-parse: "npm:^11.0.0"
|
|
||||||
remark-rehype: "npm:^11.0.0"
|
|
||||||
unified: "npm:^11.0.0"
|
|
||||||
unist-util-visit: "npm:^5.0.0"
|
|
||||||
vfile: "npm:^6.0.0"
|
|
||||||
peerDependencies:
|
|
||||||
"@types/react": ">=18"
|
|
||||||
react: ">=18"
|
|
||||||
checksum: 10c0/4a5dc7d15ca6d05e9ee95318c1904f83b111a76f7588c44f50f1d54d4c97193b84e4f64c4b592057c989228238a2590306cedd0c4d398e75da49262b2b5ae1bf
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-redux@npm:8.x.x || 9.x.x":
|
"react-redux@npm:8.x.x || 9.x.x":
|
||||||
version: 9.3.0
|
version: 9.3.0
|
||||||
resolution: "react-redux@npm:9.3.0"
|
resolution: "react-redux@npm:9.3.0"
|
||||||
@@ -5043,7 +4460,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"remark-rehype@npm:^11.0.0, remark-rehype@npm:^11.1.2":
|
"remark-rehype@npm:^11.1.2":
|
||||||
version: 11.1.2
|
version: 11.1.2
|
||||||
resolution: "remark-rehype@npm:11.1.2"
|
resolution: "remark-rehype@npm:11.1.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5377,13 +4794,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"tr46@npm:~0.0.3":
|
|
||||||
version: 0.0.3
|
|
||||||
resolution: "tr46@npm:0.0.3"
|
|
||||||
checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"trim-lines@npm:^3.0.0":
|
"trim-lines@npm:^3.0.0":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "trim-lines@npm:3.0.1"
|
resolution: "trim-lines@npm:3.0.1"
|
||||||
@@ -5412,13 +4822,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"typescript-event-target@npm:^1.1.1":
|
|
||||||
version: 1.1.2
|
|
||||||
resolution: "typescript-event-target@npm:1.1.2"
|
|
||||||
checksum: 10c0/8aebaf92c8a288242dd977c99061a75a37f65a6863298ba8d58ee77a62d5bb67ee301125563f52cd3a3be408055f205bf5faa6a273c6d79d167bdc60752cb2aa
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"typescript@npm:^5.7.3":
|
"typescript@npm:^5.7.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "typescript@npm:5.9.3"
|
resolution: "typescript@npm:5.9.3"
|
||||||
@@ -5636,23 +5039,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"webidl-conversions@npm:^3.0.0":
|
|
||||||
version: 3.0.1
|
|
||||||
resolution: "webidl-conversions@npm:3.0.1"
|
|
||||||
checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"whatwg-url@npm:^5.0.0":
|
|
||||||
version: 5.0.0
|
|
||||||
resolution: "whatwg-url@npm:5.0.0"
|
|
||||||
dependencies:
|
|
||||||
tr46: "npm:~0.0.3"
|
|
||||||
webidl-conversions: "npm:^3.0.0"
|
|
||||||
checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"which@npm:^2.0.1":
|
"which@npm:^2.0.1":
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
resolution: "which@npm:2.0.2"
|
resolution: "which@npm:2.0.2"
|
||||||
@@ -5664,22 +5050,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"zod-to-json-schema@npm:^3.20.0, zod-to-json-schema@npm:^3.23.0":
|
|
||||||
version: 3.25.2
|
|
||||||
resolution: "zod-to-json-schema@npm:3.25.2"
|
|
||||||
peerDependencies:
|
|
||||||
zod: ^3.25.28 || ^4
|
|
||||||
checksum: 10c0/dd300554393903022487688af14fbda5c719ba8179702bb55b3aa86318830467f0f7beb7d654036975ac963dc4843b72e59636448bfff9a0608f277bb6a14939
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"zod@npm:^3.22.0, zod@npm:^3.23.8":
|
|
||||||
version: 3.25.76
|
|
||||||
resolution: "zod@npm:3.25.76"
|
|
||||||
checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"zod@npm:^4.3.5":
|
"zod@npm:^4.3.5":
|
||||||
version: 4.6.4
|
version: 4.6.4
|
||||||
resolution: "zod@npm:4.6.4"
|
resolution: "zod@npm:4.6.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user