Implemented Inspiration-agent app, a chat interface for interacting with repoguide so viewers of the repository have a nicer interface to access him through
This commit is contained in:
parent
11815fc119
commit
de26e3464d
2413
inspiration-engine/package-lock.json
generated
2413
inspiration-engine/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@icons-pack/react-simple-icons": "^13.8.0",
|
||||||
"@opennextjs/cloudflare": "^1.3.0",
|
"@opennextjs/cloudflare": "^1.3.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||||
@ -28,11 +29,15 @@
|
|||||||
"@radix-ui/react-slider": "^1.3.6",
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||||
|
"ai": "^5.0.60",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"color": "^5.0.2",
|
"color": "^5.0.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"harden-react-markdown": "^1.1.2",
|
||||||
|
"katex": "^0.16.23",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"next": "15.4.6",
|
"next": "15.4.6",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
@ -41,7 +46,14 @@
|
|||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hook-form": "^7.64.0",
|
"react-hook-form": "^7.64.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"react-syntax-highlighter": "^15.6.6",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
|
"shiki": "^3.13.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"use-stick-to-bottom": "^1.1.1",
|
||||||
"zod": "^4.1.11"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -51,6 +63,7 @@
|
|||||||
"@types/node": "^20.19.19",
|
"@types/node": "^20.19.19",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.4.6",
|
"eslint-config-next": "15.4.6",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
|||||||
@ -31,7 +31,6 @@ export default function Home() {
|
|||||||
{ id: 17, height: 240, width: 'normal' },
|
{ id: 17, height: 240, width: 'normal' },
|
||||||
{ id: 18, height: 180, width: 'normal' },
|
{ id: 18, height: 180, width: 'normal' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Mock data for collections
|
// Mock data for collections
|
||||||
const mockCollections = [
|
const mockCollections = [
|
||||||
{ id: 1, name: 'Brand Identity', itemCount: 47 },
|
{ id: 1, name: 'Brand Identity', itemCount: 47 },
|
||||||
|
|||||||
@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/tooltip';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type ActionsProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const Actions = ({ className, children, ...props }: ActionsProps) => (
|
||||||
|
<div className={cn('flex items-center gap-1', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ActionProps = ComponentProps<typeof Button> & {
|
||||||
|
tooltip?: string;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Action = ({
|
||||||
|
tooltip,
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
variant = 'ghost',
|
||||||
|
size = 'sm',
|
||||||
|
...props
|
||||||
|
}: ActionProps) => {
|
||||||
|
const button = (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'size-9 p-1.5 text-muted-foreground hover:text-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
size={size}
|
||||||
|
type="button"
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<span className="sr-only">{label || tooltip}</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{tooltip}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return button;
|
||||||
|
};
|
||||||
212
inspiration-engine/src/components/ui/shadcn-io/ai/branch.tsx
Normal file
212
inspiration-engine/src/components/ui/shadcn-io/ai/branch.tsx
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { UIMessage } from 'ai';
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps, HTMLAttributes, ReactElement } from 'react';
|
||||||
|
import { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type BranchContextType = {
|
||||||
|
currentBranch: number;
|
||||||
|
totalBranches: number;
|
||||||
|
goToPrevious: () => void;
|
||||||
|
goToNext: () => void;
|
||||||
|
branches: ReactElement[];
|
||||||
|
setBranches: (branches: ReactElement[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BranchContext = createContext<BranchContextType | null>(null);
|
||||||
|
|
||||||
|
const useBranch = () => {
|
||||||
|
const context = useContext(BranchContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('Branch components must be used within Branch');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
defaultBranch?: number;
|
||||||
|
onBranchChange?: (branchIndex: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Branch = ({
|
||||||
|
defaultBranch = 0,
|
||||||
|
onBranchChange,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BranchProps) => {
|
||||||
|
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
|
||||||
|
const [branches, setBranches] = useState<ReactElement[]>([]);
|
||||||
|
|
||||||
|
const handleBranchChange = (newBranch: number) => {
|
||||||
|
setCurrentBranch(newBranch);
|
||||||
|
onBranchChange?.(newBranch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToPrevious = () => {
|
||||||
|
const newBranch =
|
||||||
|
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
|
||||||
|
handleBranchChange(newBranch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToNext = () => {
|
||||||
|
const newBranch =
|
||||||
|
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
|
||||||
|
handleBranchChange(newBranch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextValue: BranchContextType = {
|
||||||
|
currentBranch,
|
||||||
|
totalBranches: branches.length,
|
||||||
|
goToPrevious,
|
||||||
|
goToNext,
|
||||||
|
branches,
|
||||||
|
setBranches,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BranchContext.Provider value={contextValue}>
|
||||||
|
<div
|
||||||
|
className={cn('grid w-full gap-2 [&>div]:pb-0', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</BranchContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchMessagesProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const BranchMessages = ({ children, ...props }: BranchMessagesProps) => {
|
||||||
|
const { currentBranch, setBranches, branches } = useBranch();
|
||||||
|
const childrenArray = Array.isArray(children) ? children : [children];
|
||||||
|
|
||||||
|
// Use useEffect to update branches when they change
|
||||||
|
useEffect(() => {
|
||||||
|
if (branches.length !== childrenArray.length) {
|
||||||
|
setBranches(childrenArray);
|
||||||
|
}
|
||||||
|
}, [childrenArray, branches, setBranches]);
|
||||||
|
|
||||||
|
return childrenArray.map((branch, index) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'grid gap-2 overflow-hidden [&>div]:pb-0',
|
||||||
|
index === currentBranch ? 'block' : 'hidden'
|
||||||
|
)}
|
||||||
|
key={branch.key}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{branch}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
from: UIMessage['role'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BranchSelector = ({
|
||||||
|
className,
|
||||||
|
from,
|
||||||
|
...props
|
||||||
|
}: BranchSelectorProps) => {
|
||||||
|
const { totalBranches } = useBranch();
|
||||||
|
|
||||||
|
// Don't render if there's only one branch
|
||||||
|
if (totalBranches <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 self-end px-10',
|
||||||
|
from === 'assistant' ? 'justify-start' : 'justify-end',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchPreviousProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
|
export const BranchPrevious = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: BranchPreviousProps) => {
|
||||||
|
const { goToPrevious, totalBranches } = useBranch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
aria-label="Previous branch"
|
||||||
|
className={cn(
|
||||||
|
'size-7 shrink-0 rounded-full text-muted-foreground transition-colors',
|
||||||
|
'hover:bg-accent hover:text-foreground',
|
||||||
|
'disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={totalBranches <= 1}
|
||||||
|
onClick={goToPrevious}
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronLeftIcon size={14} />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchNextProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
|
export const BranchNext = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: BranchNextProps) => {
|
||||||
|
const { goToNext, totalBranches } = useBranch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
aria-label="Next branch"
|
||||||
|
className={cn(
|
||||||
|
'size-7 shrink-0 rounded-full text-muted-foreground transition-colors',
|
||||||
|
'hover:bg-accent hover:text-foreground',
|
||||||
|
'disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={totalBranches <= 1}
|
||||||
|
onClick={goToNext}
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronRightIcon size={14} />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchPageProps = HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|
||||||
|
export const BranchPage = ({ className, ...props }: BranchPageProps) => {
|
||||||
|
const { currentBranch, totalBranches } = useBranch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'font-medium text-muted-foreground text-xs tabular-nums',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{currentBranch + 1} of {totalBranches}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
148
inspiration-engine/src/components/ui/shadcn-io/ai/code-block.tsx
Normal file
148
inspiration-engine/src/components/ui/shadcn-io/ai/code-block.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { CheckIcon, CopyIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps, HTMLAttributes, ReactNode } from 'react';
|
||||||
|
import { createContext, useContext, useState } from 'react';
|
||||||
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
|
import {
|
||||||
|
oneDark,
|
||||||
|
oneLight,
|
||||||
|
} from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||||
|
|
||||||
|
type CodeBlockContextType = {
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CodeBlockContext = createContext<CodeBlockContextType>({
|
||||||
|
code: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
code: string;
|
||||||
|
language: string;
|
||||||
|
showLineNumbers?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlock = ({
|
||||||
|
code,
|
||||||
|
language,
|
||||||
|
showLineNumbers = false,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CodeBlockProps) => (
|
||||||
|
<CodeBlockContext.Provider value={{ code }}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative w-full overflow-hidden rounded-md border bg-background text-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<SyntaxHighlighter
|
||||||
|
className="overflow-hidden dark:hidden"
|
||||||
|
codeTagProps={{
|
||||||
|
className: 'font-mono text-sm',
|
||||||
|
}}
|
||||||
|
customStyle={{
|
||||||
|
margin: 0,
|
||||||
|
padding: '1rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
color: 'hsl(var(--foreground))',
|
||||||
|
}}
|
||||||
|
language={language}
|
||||||
|
lineNumberStyle={{
|
||||||
|
color: 'hsl(var(--muted-foreground))',
|
||||||
|
paddingRight: '1rem',
|
||||||
|
minWidth: '2.5rem',
|
||||||
|
}}
|
||||||
|
showLineNumbers={showLineNumbers}
|
||||||
|
style={oneLight}
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
className="hidden overflow-hidden dark:block"
|
||||||
|
codeTagProps={{
|
||||||
|
className: 'font-mono text-sm',
|
||||||
|
}}
|
||||||
|
customStyle={{
|
||||||
|
margin: 0,
|
||||||
|
padding: '1rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
color: 'hsl(var(--foreground))',
|
||||||
|
}}
|
||||||
|
language={language}
|
||||||
|
lineNumberStyle={{
|
||||||
|
color: 'hsl(var(--muted-foreground))',
|
||||||
|
paddingRight: '1rem',
|
||||||
|
minWidth: '2.5rem',
|
||||||
|
}}
|
||||||
|
showLineNumbers={showLineNumbers}
|
||||||
|
style={oneDark}
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
{children && (
|
||||||
|
<div className="absolute top-2 right-2 flex items-center gap-2">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CodeBlockContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
||||||
|
onCopy?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockCopyButton = ({
|
||||||
|
onCopy,
|
||||||
|
onError,
|
||||||
|
timeout = 2000,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockCopyButtonProps) => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const { code } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
const copyToClipboard = async () => {
|
||||||
|
if (typeof window === 'undefined' || !navigator.clipboard.writeText) {
|
||||||
|
onError?.(new Error('Clipboard API not available'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(code);
|
||||||
|
setIsCopied(true);
|
||||||
|
onCopy?.();
|
||||||
|
setTimeout(() => setIsCopied(false), timeout);
|
||||||
|
} catch (error) {
|
||||||
|
onError?.(error as Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn('shrink-0', className)}
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <Icon size={14} />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { ArrowDownIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';
|
||||||
|
|
||||||
|
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
||||||
|
|
||||||
|
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||||
|
<StickToBottom
|
||||||
|
className={cn('relative flex-1 overflow-y-auto', className)}
|
||||||
|
initial="smooth"
|
||||||
|
resize="smooth"
|
||||||
|
role="log"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ConversationContentProps = ComponentProps<
|
||||||
|
typeof StickToBottom.Content
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const ConversationContent = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ConversationContentProps) => (
|
||||||
|
<StickToBottom.Content className={cn('p-4', className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
|
export const ConversationScrollButton = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ConversationScrollButtonProps) => {
|
||||||
|
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
||||||
|
|
||||||
|
const handleScrollToBottom = useCallback(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [scrollToBottom]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
!isAtBottom && (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={handleScrollToBottom}
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowDownIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
24
inspiration-engine/src/components/ui/shadcn-io/ai/image.tsx
Normal file
24
inspiration-engine/src/components/ui/shadcn-io/ai/image.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { Experimental_GeneratedImage } from 'ai';
|
||||||
|
|
||||||
|
export type ImageProps = Experimental_GeneratedImage & {
|
||||||
|
className?: string;
|
||||||
|
alt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Image = ({
|
||||||
|
base64,
|
||||||
|
uint8Array,
|
||||||
|
mediaType,
|
||||||
|
...props
|
||||||
|
}: ImageProps) => (
|
||||||
|
<img
|
||||||
|
{...props}
|
||||||
|
alt={props.alt}
|
||||||
|
className={cn(
|
||||||
|
'h-auto max-w-full overflow-hidden rounded-md',
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
src={`data:${mediaType};base64,${base64}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
@ -0,0 +1,283 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Badge } from '@repo/shadcn-ui/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
type CarouselApi,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/carousel';
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/hover-card';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';
|
||||||
|
import { type ComponentProps, useCallback, useEffect, useState, useRef, createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
// Context to share carousel API with child components
|
||||||
|
const CarouselApiContext = createContext<CarouselApi | undefined>(undefined);
|
||||||
|
|
||||||
|
// Hook to access carousel API from the nearest InlineCitationCarousel parent
|
||||||
|
const useCarouselApi = () => {
|
||||||
|
const api = useContext(CarouselApiContext);
|
||||||
|
return api;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InlineCitationProps = ComponentProps<'span'>;
|
||||||
|
|
||||||
|
export const InlineCitation = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationProps) => (
|
||||||
|
<span
|
||||||
|
className={cn('group inline items-center gap-1', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationTextProps = ComponentProps<'span'>;
|
||||||
|
|
||||||
|
export const InlineCitationText = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationTextProps) => (
|
||||||
|
<span
|
||||||
|
className={cn('transition-colors group-hover:bg-accent', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCardProps = ComponentProps<typeof HoverCard>;
|
||||||
|
|
||||||
|
export const InlineCitationCard = (props: InlineCitationCardProps) => (
|
||||||
|
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & {
|
||||||
|
sources: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InlineCitationCardTrigger = ({
|
||||||
|
sources,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCardTriggerProps) => (
|
||||||
|
<HoverCardTrigger asChild>
|
||||||
|
<Badge
|
||||||
|
className={cn('ml-1 rounded-full', className)}
|
||||||
|
variant="secondary"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{sources.length ? (
|
||||||
|
<>
|
||||||
|
{new URL(sources[0]).hostname}{' '}
|
||||||
|
{sources.length > 1 && `+${sources.length - 1}`}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'unknown'
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCardBodyProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const InlineCitationCardBody = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCardBodyProps) => (
|
||||||
|
<HoverCardContent className={cn('relative w-80 p-0', className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCarouselProps = ComponentProps<typeof Carousel>;
|
||||||
|
|
||||||
|
export const InlineCitationCarousel = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselProps) => {
|
||||||
|
const [api, setApi] = useState<CarouselApi>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselApiContext.Provider value={api}>
|
||||||
|
<Carousel
|
||||||
|
className={cn('w-full', className)}
|
||||||
|
setApi={setApi}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Carousel>
|
||||||
|
</CarouselApiContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InlineCitationCarouselContentProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselContent = (
|
||||||
|
props: InlineCitationCarouselContentProps
|
||||||
|
) => <CarouselContent {...props} />;
|
||||||
|
|
||||||
|
export type InlineCitationCarouselItemProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselItem = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselItemProps) => (
|
||||||
|
<CarouselItem className={cn('w-full space-y-2 p-4', className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCarouselHeaderProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselHeaderProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCarouselIndexProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselIndex = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselIndexProps) => {
|
||||||
|
const api = useCarouselApi();
|
||||||
|
const [current, setCurrent] = useState(0);
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCount(api.scrollSnapList().length);
|
||||||
|
setCurrent(api.selectedScrollSnap() + 1);
|
||||||
|
|
||||||
|
api.on('select', () => {
|
||||||
|
setCurrent(api.selectedScrollSnap() + 1);
|
||||||
|
});
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? `${current}/${count}`}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InlineCitationCarouselPrevProps = ComponentProps<'button'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselPrev = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselPrevProps) => {
|
||||||
|
const api = useCarouselApi();
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (api) {
|
||||||
|
api.scrollPrev();
|
||||||
|
}
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-label="Previous"
|
||||||
|
className={cn('shrink-0', className)}
|
||||||
|
onClick={handleClick}
|
||||||
|
type="button"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon className="size-4 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InlineCitationCarouselNextProps = ComponentProps<'button'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselNext = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselNextProps) => {
|
||||||
|
const api = useCarouselApi();
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (api) {
|
||||||
|
api.scrollNext();
|
||||||
|
}
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-label="Next"
|
||||||
|
className={cn('shrink-0', className)}
|
||||||
|
onClick={handleClick}
|
||||||
|
type="button"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowRightIcon className="size-4 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InlineCitationSourceProps = ComponentProps<'div'> & {
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InlineCitationSource = ({
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
description,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: InlineCitationSourceProps) => (
|
||||||
|
<div className={cn('space-y-1', className)} {...props}>
|
||||||
|
{title && (
|
||||||
|
<h4 className="truncate font-medium text-sm leading-tight">{title}</h4>
|
||||||
|
)}
|
||||||
|
{url && (
|
||||||
|
<p className="truncate break-all text-muted-foreground text-xs">{url}</p>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationQuoteProps = ComponentProps<'blockquote'>;
|
||||||
|
|
||||||
|
export const InlineCitationQuote = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationQuoteProps) => (
|
||||||
|
<blockquote
|
||||||
|
className={cn(
|
||||||
|
'border-muted border-l-2 pl-3 text-muted-foreground text-sm italic',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
);
|
||||||
96
inspiration-engine/src/components/ui/shadcn-io/ai/loader.tsx
Normal file
96
inspiration-engine/src/components/ui/shadcn-io/ai/loader.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
type LoaderIconProps = {
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
|
||||||
|
<svg
|
||||||
|
height={size}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{ color: 'currentcolor' }}
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width={size}
|
||||||
|
>
|
||||||
|
<title>Loader</title>
|
||||||
|
<g clipPath="url(#clip0_2393_1490)">
|
||||||
|
<path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" />
|
||||||
|
<path
|
||||||
|
d="M8 16V12"
|
||||||
|
opacity="0.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.29773 1.52783L5.64887 4.7639"
|
||||||
|
opacity="0.9"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.7023 1.52783L10.3511 4.7639"
|
||||||
|
opacity="0.1"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.7023 14.472L10.3511 11.236"
|
||||||
|
opacity="0.4"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.29773 14.472L5.64887 11.236"
|
||||||
|
opacity="0.6"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15.6085 5.52783L11.8043 6.7639"
|
||||||
|
opacity="0.2"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0.391602 10.472L4.19583 9.23598"
|
||||||
|
opacity="0.7"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15.6085 10.4722L11.8043 9.2361"
|
||||||
|
opacity="0.3"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0.391602 5.52783L4.19583 6.7639"
|
||||||
|
opacity="0.8"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2393_1490">
|
||||||
|
<rect fill="white" height="16" width="16" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'inline-flex animate-spin items-center justify-center',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<LoaderIcon size={size} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/avatar';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { UIMessage } from 'ai';
|
||||||
|
import type { ComponentProps, HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
from: UIMessage['role'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Message = ({ className, from, ...props }: MessageProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'group flex w-full items-end justify-end gap-2 py-4',
|
||||||
|
from === 'user' ? 'is-user' : 'is-assistant flex-row-reverse justify-end',
|
||||||
|
'[&>div]:max-w-[80%]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const MessageContent = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: MessageContentProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col gap-2 overflow-hidden rounded-lg px-4 py-3 text-foreground text-sm',
|
||||||
|
'group-[.is-user]:bg-primary group-[.is-user]:text-primary-foreground',
|
||||||
|
'group-[.is-assistant]:bg-secondary group-[.is-assistant]:text-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="is-user:dark">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type MessageAvatarProps = ComponentProps<typeof Avatar> & {
|
||||||
|
src: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MessageAvatar = ({
|
||||||
|
src,
|
||||||
|
name,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: MessageAvatarProps) => (
|
||||||
|
<Avatar
|
||||||
|
className={cn('size-8 ring ring-1 ring-border', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<AvatarImage alt="" className="mt-0 mb-0" src={src} />
|
||||||
|
<AvatarFallback>{name?.slice(0, 2) || 'ME'}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
@ -0,0 +1,225 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/select';
|
||||||
|
import { Textarea } from '@repo/shadcn-ui/components/ui/textarea';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { ChatStatus } from 'ai';
|
||||||
|
import { Loader2Icon, SendIcon, SquareIcon, XIcon } from 'lucide-react';
|
||||||
|
import type {
|
||||||
|
ComponentProps,
|
||||||
|
HTMLAttributes,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
} from 'react';
|
||||||
|
import { Children } from 'react';
|
||||||
|
|
||||||
|
export type PromptInputProps = HTMLAttributes<HTMLFormElement>;
|
||||||
|
|
||||||
|
export const PromptInput = ({ className, ...props }: PromptInputProps) => (
|
||||||
|
<form
|
||||||
|
className={cn(
|
||||||
|
'w-full divide-y overflow-hidden rounded-xl border bg-background shadow-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputTextareaProps = ComponentProps<typeof Textarea> & {
|
||||||
|
minHeight?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PromptInputTextarea = ({
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
placeholder = 'What would you like to know?',
|
||||||
|
minHeight = 48,
|
||||||
|
maxHeight = 164,
|
||||||
|
...props
|
||||||
|
}: PromptInputTextareaProps) => {
|
||||||
|
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
// Allow newline
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit on Enter (without Shift)
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.currentTarget.form;
|
||||||
|
if (form) {
|
||||||
|
form.requestSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
className={cn(
|
||||||
|
'w-full resize-none rounded-none border-none p-3 shadow-none outline-none ring-0',
|
||||||
|
'field-sizing-content max-h-[6lh] bg-transparent dark:bg-transparent',
|
||||||
|
'focus-visible:ring-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
name="message"
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange?.(e);
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PromptInputToolbarProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const PromptInputToolbar = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputToolbarProps) => (
|
||||||
|
<div
|
||||||
|
className={cn('flex items-center justify-between p-1', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const PromptInputTools = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputToolsProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1',
|
||||||
|
'[&_button:first-child]:rounded-bl-xl',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputButtonProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
|
export const PromptInputButton = ({
|
||||||
|
variant = 'ghost',
|
||||||
|
className,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: PromptInputButtonProps) => {
|
||||||
|
const newSize =
|
||||||
|
(size ?? Children.count(props.children) > 1) ? 'default' : 'icon';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 gap-1.5 rounded-lg',
|
||||||
|
variant === 'ghost' && 'text-muted-foreground',
|
||||||
|
newSize === 'default' && 'px-3',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
size={newSize}
|
||||||
|
type="button"
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PromptInputSubmitProps = ComponentProps<typeof Button> & {
|
||||||
|
status?: ChatStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PromptInputSubmit = ({
|
||||||
|
className,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'icon',
|
||||||
|
status,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: PromptInputSubmitProps) => {
|
||||||
|
let Icon = <SendIcon className="size-4" />;
|
||||||
|
|
||||||
|
if (status === 'submitted') {
|
||||||
|
Icon = <Loader2Icon className="size-4 animate-spin" />;
|
||||||
|
} else if (status === 'streaming') {
|
||||||
|
Icon = <SquareIcon className="size-4" />;
|
||||||
|
} else if (status === 'error') {
|
||||||
|
Icon = <XIcon className="size-4" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn('gap-1.5 rounded-lg', className)}
|
||||||
|
size={size}
|
||||||
|
type="submit"
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? Icon}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PromptInputModelSelectProps = ComponentProps<typeof Select>;
|
||||||
|
|
||||||
|
export const PromptInputModelSelect = (props: PromptInputModelSelectProps) => (
|
||||||
|
<Select {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputModelSelectTriggerProps = ComponentProps<
|
||||||
|
typeof SelectTrigger
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const PromptInputModelSelectTrigger = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputModelSelectTriggerProps) => (
|
||||||
|
<SelectTrigger
|
||||||
|
className={cn(
|
||||||
|
'border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors',
|
||||||
|
'hover:bg-accent hover:text-foreground [&[aria-expanded="true"]]:bg-accent [&[aria-expanded="true"]]:text-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputModelSelectContentProps = ComponentProps<
|
||||||
|
typeof SelectContent
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const PromptInputModelSelectContent = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputModelSelectContentProps) => (
|
||||||
|
<SelectContent className={cn(className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputModelSelectItemProps = ComponentProps<typeof SelectItem>;
|
||||||
|
|
||||||
|
export const PromptInputModelSelectItem = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputModelSelectItemProps) => (
|
||||||
|
<SelectItem className={cn(className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputModelSelectValueProps = ComponentProps<
|
||||||
|
typeof SelectValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const PromptInputModelSelectValue = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputModelSelectValueProps) => (
|
||||||
|
<SelectValue className={cn(className)} {...props} />
|
||||||
|
);
|
||||||
180
inspiration-engine/src/components/ui/shadcn-io/ai/reasoning.tsx
Normal file
180
inspiration-engine/src/components/ui/shadcn-io/ai/reasoning.tsx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/collapsible';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { BrainIcon, ChevronDownIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
import { createContext, memo, useContext, useEffect, useState } from 'react';
|
||||||
|
import { Response } from './response';
|
||||||
|
|
||||||
|
type ReasoningContextValue = {
|
||||||
|
isStreaming: boolean;
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (open: boolean) => void;
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
||||||
|
|
||||||
|
const useReasoning = () => {
|
||||||
|
const context = useContext(ReasoningContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('Reasoning components must be used within Reasoning');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
||||||
|
isStreaming?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
duration?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AUTO_CLOSE_DELAY = 1000;
|
||||||
|
|
||||||
|
export const Reasoning = memo(
|
||||||
|
({
|
||||||
|
className,
|
||||||
|
isStreaming = false,
|
||||||
|
open,
|
||||||
|
defaultOpen = false,
|
||||||
|
onOpenChange,
|
||||||
|
duration: durationProp,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ReasoningProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useControllableState({
|
||||||
|
prop: open,
|
||||||
|
defaultProp: defaultOpen,
|
||||||
|
onChange: onOpenChange,
|
||||||
|
});
|
||||||
|
const [duration, setDuration] = useControllableState({
|
||||||
|
prop: durationProp,
|
||||||
|
defaultProp: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [hasAutoClosedRef, setHasAutoClosedRef] = useState(false);
|
||||||
|
const [startTime, setStartTime] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Track duration when streaming starts and ends
|
||||||
|
useEffect(() => {
|
||||||
|
if (isStreaming) {
|
||||||
|
if (startTime === null) {
|
||||||
|
setStartTime(Date.now());
|
||||||
|
}
|
||||||
|
} else if (startTime !== null) {
|
||||||
|
setDuration(Math.round((Date.now() - startTime) / 1000));
|
||||||
|
setStartTime(null);
|
||||||
|
}
|
||||||
|
}, [isStreaming, startTime, setDuration]);
|
||||||
|
|
||||||
|
// Auto-open when streaming starts, auto-close when streaming ends (once only)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isStreaming && !isOpen) {
|
||||||
|
setIsOpen(true);
|
||||||
|
} else if (!isStreaming && isOpen && !defaultOpen && !hasAutoClosedRef) {
|
||||||
|
// Add a small delay before closing to allow user to see the content
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setHasAutoClosedRef(true);
|
||||||
|
}, AUTO_CLOSE_DELAY);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosedRef]);
|
||||||
|
|
||||||
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
|
setIsOpen(newOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReasoningContext.Provider
|
||||||
|
value={{ isStreaming, isOpen, setIsOpen, duration }}
|
||||||
|
>
|
||||||
|
<Collapsible
|
||||||
|
className={cn('not-prose mb-4', className)}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
open={isOpen}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Collapsible>
|
||||||
|
</ReasoningContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ReasoningTriggerProps = ComponentProps<
|
||||||
|
typeof CollapsibleTrigger
|
||||||
|
> & {
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReasoningTrigger = memo(
|
||||||
|
({
|
||||||
|
className,
|
||||||
|
title = 'Reasoning',
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ReasoningTriggerProps) => {
|
||||||
|
const { isStreaming, isOpen, duration } = useReasoning();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsibleTrigger
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 text-muted-foreground text-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? (
|
||||||
|
<>
|
||||||
|
<BrainIcon className="size-4" />
|
||||||
|
{isStreaming || duration === 0 ? (
|
||||||
|
<p>Thinking...</p>
|
||||||
|
) : (
|
||||||
|
<p>Thought for {duration} seconds</p>
|
||||||
|
)}
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={cn(
|
||||||
|
'size-4 text-muted-foreground transition-transform',
|
||||||
|
isOpen ? 'rotate-180' : 'rotate-0'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ReasoningContentProps = ComponentProps<
|
||||||
|
typeof CollapsibleContent
|
||||||
|
> & {
|
||||||
|
children: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReasoningContent = memo(
|
||||||
|
({ className, children, ...props }: ReasoningContentProps) => (
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(
|
||||||
|
'mt-4 text-sm',
|
||||||
|
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Response className="grid gap-2">{children}</Response>
|
||||||
|
</CollapsibleContent>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Reasoning.displayName = 'Reasoning';
|
||||||
|
ReasoningTrigger.displayName = 'ReasoningTrigger';
|
||||||
|
ReasoningContent.displayName = 'ReasoningContent';
|
||||||
392
inspiration-engine/src/components/ui/shadcn-io/ai/response.tsx
Normal file
392
inspiration-engine/src/components/ui/shadcn-io/ai/response.tsx
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { ComponentProps, HTMLAttributes } from 'react';
|
||||||
|
import { isValidElement, memo } from 'react';
|
||||||
|
import ReactMarkdown, { type Options } from 'react-markdown';
|
||||||
|
import rehypeKatex from 'rehype-katex';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import remarkMath from 'remark-math';
|
||||||
|
import { CodeBlock, CodeBlockCopyButton } from './code-block';
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
import hardenReactMarkdown from 'harden-react-markdown';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses markdown text and removes incomplete tokens to prevent partial rendering
|
||||||
|
* of links, images, bold, and italic formatting during streaming.
|
||||||
|
*/
|
||||||
|
function parseIncompleteMarkdown(text: string): string {
|
||||||
|
if (!text || typeof text !== 'string') {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = text;
|
||||||
|
|
||||||
|
// Handle incomplete links and images
|
||||||
|
// Pattern: [...] or ![...] where the closing ] is missing
|
||||||
|
const linkImagePattern = /(!?\[)([^\]]*?)$/;
|
||||||
|
const linkMatch = result.match(linkImagePattern);
|
||||||
|
if (linkMatch) {
|
||||||
|
// If we have an unterminated [ or ![, remove it and everything after
|
||||||
|
const startIndex = result.lastIndexOf(linkMatch[1]);
|
||||||
|
result = result.substring(0, startIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete bold formatting (**)
|
||||||
|
const boldPattern = /(\*\*)([^*]*?)$/;
|
||||||
|
const boldMatch = result.match(boldPattern);
|
||||||
|
if (boldMatch) {
|
||||||
|
// Count the number of ** in the entire string
|
||||||
|
const asteriskPairs = (result.match(/\*\*/g) || []).length;
|
||||||
|
// If odd number of **, we have an incomplete bold - complete it
|
||||||
|
if (asteriskPairs % 2 === 1) {
|
||||||
|
result = `${result}**`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete italic formatting (__)
|
||||||
|
const italicPattern = /(__)([^_]*?)$/;
|
||||||
|
const italicMatch = result.match(italicPattern);
|
||||||
|
if (italicMatch) {
|
||||||
|
// Count the number of __ in the entire string
|
||||||
|
const underscorePairs = (result.match(/__/g) || []).length;
|
||||||
|
// If odd number of __, we have an incomplete italic - complete it
|
||||||
|
if (underscorePairs % 2 === 1) {
|
||||||
|
result = `${result}__`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete single asterisk italic (*)
|
||||||
|
const singleAsteriskPattern = /(\*)([^*]*?)$/;
|
||||||
|
const singleAsteriskMatch = result.match(singleAsteriskPattern);
|
||||||
|
if (singleAsteriskMatch) {
|
||||||
|
// Count single asterisks that aren't part of **
|
||||||
|
const singleAsterisks = result.split('').reduce((acc, char, index) => {
|
||||||
|
if (char === '*') {
|
||||||
|
// Check if it's part of a ** pair
|
||||||
|
const prevChar = result[index - 1];
|
||||||
|
const nextChar = result[index + 1];
|
||||||
|
if (prevChar !== '*' && nextChar !== '*') {
|
||||||
|
return acc + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// If odd number of single *, we have an incomplete italic - complete it
|
||||||
|
if (singleAsterisks % 2 === 1) {
|
||||||
|
result = `${result}*`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete single underscore italic (_)
|
||||||
|
const singleUnderscorePattern = /(_)([^_]*?)$/;
|
||||||
|
const singleUnderscoreMatch = result.match(singleUnderscorePattern);
|
||||||
|
if (singleUnderscoreMatch) {
|
||||||
|
// Count single underscores that aren't part of __
|
||||||
|
const singleUnderscores = result.split('').reduce((acc, char, index) => {
|
||||||
|
if (char === '_') {
|
||||||
|
// Check if it's part of a __ pair
|
||||||
|
const prevChar = result[index - 1];
|
||||||
|
const nextChar = result[index + 1];
|
||||||
|
if (prevChar !== '_' && nextChar !== '_') {
|
||||||
|
return acc + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// If odd number of single _, we have an incomplete italic - complete it
|
||||||
|
if (singleUnderscores % 2 === 1) {
|
||||||
|
result = `${result}_`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete inline code blocks (`) - but avoid code blocks (```)
|
||||||
|
const inlineCodePattern = /(`)([^`]*?)$/;
|
||||||
|
const inlineCodeMatch = result.match(inlineCodePattern);
|
||||||
|
if (inlineCodeMatch) {
|
||||||
|
// Check if we're dealing with a code block (triple backticks)
|
||||||
|
const hasCodeBlockStart = result.includes('```');
|
||||||
|
const codeBlockPattern = /```[\s\S]*?```/g;
|
||||||
|
const completeCodeBlocks = (result.match(codeBlockPattern) || []).length;
|
||||||
|
const allTripleBackticks = (result.match(/```/g) || []).length;
|
||||||
|
|
||||||
|
// If we have an odd number of ``` sequences, we're inside an incomplete code block
|
||||||
|
// In this case, don't complete inline code
|
||||||
|
const insideIncompleteCodeBlock = allTripleBackticks % 2 === 1;
|
||||||
|
|
||||||
|
if (!insideIncompleteCodeBlock) {
|
||||||
|
// Count the number of single backticks that are NOT part of triple backticks
|
||||||
|
let singleBacktickCount = 0;
|
||||||
|
for (let i = 0; i < result.length; i++) {
|
||||||
|
if (result[i] === '`') {
|
||||||
|
// Check if this backtick is part of a triple backtick sequence
|
||||||
|
const isTripleStart = result.substring(i, i + 3) === '```';
|
||||||
|
const isTripleMiddle =
|
||||||
|
i > 0 && result.substring(i - 1, i + 2) === '```';
|
||||||
|
const isTripleEnd = i > 1 && result.substring(i - 2, i + 1) === '```';
|
||||||
|
|
||||||
|
if (!(isTripleStart || isTripleMiddle || isTripleEnd)) {
|
||||||
|
singleBacktickCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If odd number of single backticks, we have an incomplete inline code - complete it
|
||||||
|
if (singleBacktickCount % 2 === 1) {
|
||||||
|
result = `${result}\``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete strikethrough formatting (~~)
|
||||||
|
const strikethroughPattern = /(~~)([^~]*?)$/;
|
||||||
|
const strikethroughMatch = result.match(strikethroughPattern);
|
||||||
|
if (strikethroughMatch) {
|
||||||
|
// Count the number of ~~ in the entire string
|
||||||
|
const tildePairs = (result.match(/~~/g) || []).length;
|
||||||
|
// If odd number of ~~, we have an incomplete strikethrough - complete it
|
||||||
|
if (tildePairs % 2 === 1) {
|
||||||
|
result = `${result}~~`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a hardened version of ReactMarkdown
|
||||||
|
const HardenedMarkdown = hardenReactMarkdown(ReactMarkdown);
|
||||||
|
|
||||||
|
export type ResponseProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
options?: Options;
|
||||||
|
children: Options['children'];
|
||||||
|
allowedImagePrefixes?: ComponentProps<
|
||||||
|
ReturnType<typeof hardenReactMarkdown>
|
||||||
|
>['allowedImagePrefixes'];
|
||||||
|
allowedLinkPrefixes?: ComponentProps<
|
||||||
|
ReturnType<typeof hardenReactMarkdown>
|
||||||
|
>['allowedLinkPrefixes'];
|
||||||
|
defaultOrigin?: ComponentProps<
|
||||||
|
ReturnType<typeof hardenReactMarkdown>
|
||||||
|
>['defaultOrigin'];
|
||||||
|
parseIncompleteMarkdown?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const components: Options['components'] = {
|
||||||
|
ol: ({ node, children, className, ...props }) => (
|
||||||
|
<ol className={cn('ml-4 list-outside list-decimal', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</ol>
|
||||||
|
),
|
||||||
|
li: ({ node, children, className, ...props }) => (
|
||||||
|
<li className={cn('py-1', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
ul: ({ node, children, className, ...props }) => (
|
||||||
|
<ul className={cn('ml-4 list-outside list-disc', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
),
|
||||||
|
hr: ({ node, className, ...props }) => (
|
||||||
|
<hr className={cn('my-6 border-border', className)} {...props} />
|
||||||
|
),
|
||||||
|
strong: ({ node, children, className, ...props }) => (
|
||||||
|
<span className={cn('font-semibold', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
a: ({ node, children, className, ...props }) => (
|
||||||
|
<a
|
||||||
|
className={cn('font-medium text-primary underline', className)}
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
h1: ({ node, children, className, ...props }) => (
|
||||||
|
<h1
|
||||||
|
className={cn('mt-6 mb-2 font-semibold text-3xl', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
),
|
||||||
|
h2: ({ node, children, className, ...props }) => (
|
||||||
|
<h2
|
||||||
|
className={cn('mt-6 mb-2 font-semibold text-2xl', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
),
|
||||||
|
h3: ({ node, children, className, ...props }) => (
|
||||||
|
<h3 className={cn('mt-6 mb-2 font-semibold text-xl', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
),
|
||||||
|
h4: ({ node, children, className, ...props }) => (
|
||||||
|
<h4 className={cn('mt-6 mb-2 font-semibold text-lg', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</h4>
|
||||||
|
),
|
||||||
|
h5: ({ node, children, className, ...props }) => (
|
||||||
|
<h5
|
||||||
|
className={cn('mt-6 mb-2 font-semibold text-base', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h5>
|
||||||
|
),
|
||||||
|
h6: ({ node, children, className, ...props }) => (
|
||||||
|
<h6 className={cn('mt-6 mb-2 font-semibold text-sm', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</h6>
|
||||||
|
),
|
||||||
|
table: ({ node, children, className, ...props }) => (
|
||||||
|
<div className="my-4 overflow-x-auto">
|
||||||
|
<table
|
||||||
|
className={cn('w-full border-collapse border border-border', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ node, children, className, ...props }) => (
|
||||||
|
<thead className={cn('bg-muted/50', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</thead>
|
||||||
|
),
|
||||||
|
tbody: ({ node, children, className, ...props }) => (
|
||||||
|
<tbody className={cn('divide-y divide-border', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</tbody>
|
||||||
|
),
|
||||||
|
tr: ({ node, children, className, ...props }) => (
|
||||||
|
<tr className={cn('border-border border-b', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
),
|
||||||
|
th: ({ node, children, className, ...props }) => (
|
||||||
|
<th
|
||||||
|
className={cn('px-4 py-2 text-left font-semibold text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
),
|
||||||
|
td: ({ node, children, className, ...props }) => (
|
||||||
|
<td className={cn('px-4 py-2 text-sm', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
blockquote: ({ node, children, className, ...props }) => (
|
||||||
|
<blockquote
|
||||||
|
className={cn(
|
||||||
|
'my-4 border-muted-foreground/30 border-l-4 pl-4 text-muted-foreground italic',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
),
|
||||||
|
code: ({ node, className, ...props }) => {
|
||||||
|
const inline = node?.position?.start.line === node?.position?.end.line;
|
||||||
|
|
||||||
|
if (!inline) {
|
||||||
|
return <code className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
className={cn(
|
||||||
|
'rounded bg-muted px-1.5 py-0.5 font-mono text-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pre: ({ node, className, children }) => {
|
||||||
|
let language = 'javascript';
|
||||||
|
|
||||||
|
if (typeof node?.properties?.className === 'string') {
|
||||||
|
language = node.properties.className.replace('language-', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract code content from children safely
|
||||||
|
let code = '';
|
||||||
|
if (
|
||||||
|
isValidElement(children) &&
|
||||||
|
children.props &&
|
||||||
|
typeof (children.props as any).children === 'string'
|
||||||
|
) {
|
||||||
|
code = (children.props as any).children;
|
||||||
|
} else if (typeof children === 'string') {
|
||||||
|
code = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CodeBlock
|
||||||
|
className={cn('my-4 h-auto', className)}
|
||||||
|
code={code}
|
||||||
|
language={language}
|
||||||
|
>
|
||||||
|
<CodeBlockCopyButton
|
||||||
|
onCopy={() => console.log('Copied code to clipboard')}
|
||||||
|
onError={() => console.error('Failed to copy code to clipboard')}
|
||||||
|
/>
|
||||||
|
</CodeBlock>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Response = memo(
|
||||||
|
({
|
||||||
|
className,
|
||||||
|
options,
|
||||||
|
children,
|
||||||
|
allowedImagePrefixes,
|
||||||
|
allowedLinkPrefixes,
|
||||||
|
defaultOrigin,
|
||||||
|
parseIncompleteMarkdown: shouldParseIncompleteMarkdown = true,
|
||||||
|
...props
|
||||||
|
}: ResponseProps) => {
|
||||||
|
// Parse the children to remove incomplete markdown tokens if enabled
|
||||||
|
const parsedChildren =
|
||||||
|
typeof children === 'string' && shouldParseIncompleteMarkdown
|
||||||
|
? parseIncompleteMarkdown(children)
|
||||||
|
: children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<HardenedMarkdown
|
||||||
|
allowedImagePrefixes={allowedImagePrefixes ?? ['*']}
|
||||||
|
allowedLinkPrefixes={allowedLinkPrefixes ?? ['*']}
|
||||||
|
components={components}
|
||||||
|
defaultOrigin={defaultOrigin}
|
||||||
|
rehypePlugins={[rehypeKatex]}
|
||||||
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
|
{...options}
|
||||||
|
>
|
||||||
|
{parsedChildren}
|
||||||
|
</HardenedMarkdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(prevProps, nextProps) => prevProps.children === nextProps.children
|
||||||
|
);
|
||||||
|
|
||||||
|
Response.displayName = 'Response';
|
||||||
74
inspiration-engine/src/components/ui/shadcn-io/ai/source.tsx
Normal file
74
inspiration-engine/src/components/ui/shadcn-io/ai/source.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/collapsible';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { BookIcon, ChevronDownIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type SourcesProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const Sources = ({ className, ...props }: SourcesProps) => (
|
||||||
|
<Collapsible
|
||||||
|
className={cn('not-prose mb-4 text-primary text-xs', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SourcesTrigger = ({
|
||||||
|
className,
|
||||||
|
count,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SourcesTriggerProps) => (
|
||||||
|
<CollapsibleTrigger className="flex items-center gap-2" {...props}>
|
||||||
|
{children ?? (
|
||||||
|
<>
|
||||||
|
<p className="font-medium">Used {count} sources</p>
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
|
||||||
|
|
||||||
|
export const SourcesContent = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: SourcesContentProps) => (
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(
|
||||||
|
'mt-3 flex w-fit flex-col gap-2',
|
||||||
|
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SourceProps = ComponentProps<'a'>;
|
||||||
|
|
||||||
|
export const Source = ({ href, title, children, ...props }: SourceProps) => (
|
||||||
|
<a
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
href={href}
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? (
|
||||||
|
<>
|
||||||
|
<BookIcon className="h-4 w-4" />
|
||||||
|
<span className="block font-medium">{title}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import {
|
||||||
|
ScrollArea,
|
||||||
|
ScrollBar,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/scroll-area';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
|
||||||
|
|
||||||
|
export const Suggestions = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SuggestionsProps) => (
|
||||||
|
<ScrollArea className="w-full overflow-x-auto whitespace-nowrap" {...props}>
|
||||||
|
<div className={cn('flex w-max flex-nowrap items-center gap-2', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<ScrollBar className="hidden" orientation="horizontal" />
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SuggestionProps = Omit<ComponentProps<typeof Button>, 'onClick'> & {
|
||||||
|
suggestion: string;
|
||||||
|
onClick?: (suggestion: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Suggestion = ({
|
||||||
|
suggestion,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
variant = 'outline',
|
||||||
|
size = 'sm',
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SuggestionProps) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
onClick?.(suggestion);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn('cursor-pointer rounded-full px-4', className)}
|
||||||
|
onClick={handleClick}
|
||||||
|
size={size}
|
||||||
|
type="button"
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children || suggestion}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
94
inspiration-engine/src/components/ui/shadcn-io/ai/task.tsx
Normal file
94
inspiration-engine/src/components/ui/shadcn-io/ai/task.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/collapsible';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { ChevronDownIcon, SearchIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type TaskItemFileProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const TaskItemFile = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: TaskItemFileProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-foreground text-xs',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type TaskItemProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const TaskItem = ({ children, className, ...props }: TaskItemProps) => (
|
||||||
|
<div className={cn('text-muted-foreground text-sm', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type TaskProps = ComponentProps<typeof Collapsible>;
|
||||||
|
|
||||||
|
export const Task = ({
|
||||||
|
defaultOpen = true,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: TaskProps) => (
|
||||||
|
<Collapsible
|
||||||
|
className={cn(
|
||||||
|
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
defaultOpen={defaultOpen}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TaskTrigger = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
...props
|
||||||
|
}: TaskTriggerProps) => (
|
||||||
|
<CollapsibleTrigger asChild className={cn('group', className)} {...props}>
|
||||||
|
{children ?? (
|
||||||
|
<div className="flex cursor-pointer items-center gap-2 text-muted-foreground hover:text-foreground">
|
||||||
|
<SearchIcon className="size-4" />
|
||||||
|
<p className="text-sm">{title}</p>
|
||||||
|
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type TaskContentProps = ComponentProps<typeof CollapsibleContent>;
|
||||||
|
|
||||||
|
export const TaskContent = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: TaskContentProps) => (
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(
|
||||||
|
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="mt-4 space-y-2 border-muted border-l-2 pl-4">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
);
|
||||||
142
inspiration-engine/src/components/ui/shadcn-io/ai/tool.tsx
Normal file
142
inspiration-engine/src/components/ui/shadcn-io/ai/tool.tsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Badge } from '@repo/shadcn-ui/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/collapsible';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { ToolUIPart } from 'ai';
|
||||||
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
CircleIcon,
|
||||||
|
ClockIcon,
|
||||||
|
WrenchIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import type { ComponentProps, ReactNode } from 'react';
|
||||||
|
import { CodeBlock } from './code-block';
|
||||||
|
|
||||||
|
export type ToolProps = ComponentProps<typeof Collapsible>;
|
||||||
|
|
||||||
|
export const Tool = ({ className, ...props }: ToolProps) => (
|
||||||
|
<Collapsible
|
||||||
|
className={cn('not-prose mb-4 w-full rounded-md border', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ToolHeaderProps = {
|
||||||
|
type: ToolUIPart['type'];
|
||||||
|
state: ToolUIPart['state'];
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status: ToolUIPart['state']) => {
|
||||||
|
const labels = {
|
||||||
|
'input-streaming': 'Pending',
|
||||||
|
'input-available': 'Running',
|
||||||
|
'output-available': 'Completed',
|
||||||
|
'output-error': 'Error',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
'input-streaming': <CircleIcon className="size-4" />,
|
||||||
|
'input-available': <ClockIcon className="size-4 animate-pulse" />,
|
||||||
|
'output-available': <CheckCircleIcon className="size-4 text-green-600" />,
|
||||||
|
'output-error': <XCircleIcon className="size-4 text-red-600" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge className="rounded-full text-xs" variant="secondary">
|
||||||
|
{icons[status]}
|
||||||
|
{labels[status]}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToolHeader = ({
|
||||||
|
className,
|
||||||
|
type,
|
||||||
|
state,
|
||||||
|
...props
|
||||||
|
}: ToolHeaderProps) => (
|
||||||
|
<CollapsibleTrigger
|
||||||
|
className={cn(
|
||||||
|
'flex w-full items-center justify-between gap-4 p-3',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<WrenchIcon className="size-4 text-muted-foreground" />
|
||||||
|
<span className="font-medium text-sm">{type}</span>
|
||||||
|
{getStatusBadge(state)}
|
||||||
|
</div>
|
||||||
|
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
|
||||||
|
|
||||||
|
export const ToolContent = ({ className, ...props }: ToolContentProps) => (
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(
|
||||||
|
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ToolInputProps = ComponentProps<'div'> & {
|
||||||
|
input: ToolUIPart['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
|
||||||
|
<div className={cn('space-y-2 overflow-hidden p-4', className)} {...props}>
|
||||||
|
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
Parameters
|
||||||
|
</h4>
|
||||||
|
<div className="rounded-md bg-muted/50">
|
||||||
|
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ToolOutputProps = ComponentProps<'div'> & {
|
||||||
|
output: ReactNode;
|
||||||
|
errorText: ToolUIPart['errorText'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToolOutput = ({
|
||||||
|
className,
|
||||||
|
output,
|
||||||
|
errorText,
|
||||||
|
...props
|
||||||
|
}: ToolOutputProps) => {
|
||||||
|
if (!(output || errorText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('space-y-2 p-4', className)} {...props}>
|
||||||
|
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
{errorText ? 'Error' : 'Result'}
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'overflow-x-auto rounded-md text-xs [&_table]:w-full',
|
||||||
|
errorText
|
||||||
|
? 'bg-destructive/10 text-destructive'
|
||||||
|
: 'bg-muted/50 text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{errorText && <div>{errorText}</div>}
|
||||||
|
{output && <div>{output}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,269 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/collapsible';
|
||||||
|
import { Input } from '@repo/shadcn-ui/components/ui/input';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/tooltip';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { ChevronDownIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps, ReactNode } from 'react';
|
||||||
|
import { createContext, useContext, useState } from 'react';
|
||||||
|
|
||||||
|
export type WebPreviewContextValue = {
|
||||||
|
url: string;
|
||||||
|
setUrl: (url: string) => void;
|
||||||
|
consoleOpen: boolean;
|
||||||
|
setConsoleOpen: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);
|
||||||
|
|
||||||
|
const useWebPreview = () => {
|
||||||
|
const context = useContext(WebPreviewContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('WebPreview components must be used within a WebPreview');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebPreviewProps = ComponentProps<'div'> & {
|
||||||
|
defaultUrl?: string;
|
||||||
|
onUrlChange?: (url: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WebPreview = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
defaultUrl = '',
|
||||||
|
onUrlChange,
|
||||||
|
...props
|
||||||
|
}: WebPreviewProps) => {
|
||||||
|
const [url, setUrl] = useState(defaultUrl);
|
||||||
|
const [consoleOpen, setConsoleOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleUrlChange = (newUrl: string) => {
|
||||||
|
setUrl(newUrl);
|
||||||
|
onUrlChange?.(newUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextValue: WebPreviewContextValue = {
|
||||||
|
url,
|
||||||
|
setUrl: handleUrlChange,
|
||||||
|
consoleOpen,
|
||||||
|
setConsoleOpen,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WebPreviewContext.Provider value={contextValue}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex size-full flex-col rounded-lg border bg-card',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</WebPreviewContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebPreviewNavigationProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const WebPreviewNavigation = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: WebPreviewNavigationProps) => (
|
||||||
|
<div
|
||||||
|
className={cn('flex items-center gap-1 border-b p-2', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & {
|
||||||
|
tooltip?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WebPreviewNavigationButton = ({
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
tooltip,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: WebPreviewNavigationButtonProps) => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="h-8 w-8 p-0 hover:text-foreground"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{tooltip}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type WebPreviewUrlProps = ComponentProps<typeof Input>;
|
||||||
|
|
||||||
|
export const WebPreviewUrl = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onKeyDown,
|
||||||
|
...props
|
||||||
|
}: WebPreviewUrlProps) => {
|
||||||
|
const { url, setUrl } = useWebPreview();
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
setUrl(target.value);
|
||||||
|
}
|
||||||
|
onKeyDown?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use defaultValue for uncontrolled input when no onChange is provided
|
||||||
|
if (!onChange && !value) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
className="h-8 flex-1 text-sm"
|
||||||
|
defaultValue={url}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Enter URL..."
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
className="h-8 flex-1 text-sm"
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Enter URL..."
|
||||||
|
value={value ?? url}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebPreviewBodyProps = ComponentProps<'iframe'> & {
|
||||||
|
loading?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WebPreviewBody = ({
|
||||||
|
className,
|
||||||
|
loading,
|
||||||
|
src,
|
||||||
|
...props
|
||||||
|
}: WebPreviewBodyProps) => {
|
||||||
|
const { url } = useWebPreview();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1">
|
||||||
|
<iframe
|
||||||
|
className={cn('size-full', className)}
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
|
||||||
|
src={(src ?? url) || undefined}
|
||||||
|
title="Preview"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{loading}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebPreviewConsoleProps = ComponentProps<'div'> & {
|
||||||
|
logs?: Array<{
|
||||||
|
level: 'log' | 'warn' | 'error';
|
||||||
|
message: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WebPreviewConsole = ({
|
||||||
|
className,
|
||||||
|
logs = [],
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: WebPreviewConsoleProps) => {
|
||||||
|
const { consoleOpen, setConsoleOpen } = useWebPreview();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible
|
||||||
|
className={cn('border-t bg-muted/50 font-mono text-sm', className)}
|
||||||
|
onOpenChange={setConsoleOpen}
|
||||||
|
open={consoleOpen}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="flex w-full items-center justify-between p-4 text-left font-medium hover:bg-muted/50"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
Console
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={cn(
|
||||||
|
'h-4 w-4 transition-transform duration-200',
|
||||||
|
consoleOpen && 'rotate-180'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(
|
||||||
|
'px-4 pb-4',
|
||||||
|
'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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="max-h-48 space-y-1 overflow-y-auto">
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground">No console output</p>
|
||||||
|
) : (
|
||||||
|
logs.map((log, index) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'text-xs',
|
||||||
|
log.level === 'error' && 'text-destructive',
|
||||||
|
log.level === 'warn' && 'text-yellow-600',
|
||||||
|
log.level === 'log' && 'text-foreground'
|
||||||
|
)}
|
||||||
|
key={`${log.timestamp.getTime()}-${index}`}
|
||||||
|
>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{log.timestamp.toLocaleTimeString()}
|
||||||
|
</span>{' '}
|
||||||
|
{log.message}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,620 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type IconType,
|
||||||
|
SiAstro,
|
||||||
|
SiBiome,
|
||||||
|
SiBower,
|
||||||
|
SiBun,
|
||||||
|
SiC,
|
||||||
|
SiCircleci,
|
||||||
|
SiCoffeescript,
|
||||||
|
SiCplusplus,
|
||||||
|
SiCss,
|
||||||
|
SiCssmodules,
|
||||||
|
SiDart,
|
||||||
|
SiDocker,
|
||||||
|
SiDocusaurus,
|
||||||
|
SiDotenv,
|
||||||
|
SiEditorconfig,
|
||||||
|
SiEslint,
|
||||||
|
SiGatsby,
|
||||||
|
SiGitignoredotio,
|
||||||
|
SiGnubash,
|
||||||
|
SiGo,
|
||||||
|
SiGraphql,
|
||||||
|
SiGrunt,
|
||||||
|
SiGulp,
|
||||||
|
SiHandlebarsdotjs,
|
||||||
|
SiHtml5,
|
||||||
|
SiJavascript,
|
||||||
|
SiJest,
|
||||||
|
SiJson,
|
||||||
|
SiLess,
|
||||||
|
SiMarkdown,
|
||||||
|
SiMdx,
|
||||||
|
SiMintlify,
|
||||||
|
SiMocha,
|
||||||
|
SiMysql,
|
||||||
|
SiNextdotjs,
|
||||||
|
SiPerl,
|
||||||
|
SiPhp,
|
||||||
|
SiPostcss,
|
||||||
|
SiPrettier,
|
||||||
|
SiPrisma,
|
||||||
|
SiPug,
|
||||||
|
SiPython,
|
||||||
|
SiR,
|
||||||
|
SiReact,
|
||||||
|
SiReadme,
|
||||||
|
SiRedis,
|
||||||
|
SiRemix,
|
||||||
|
SiRive,
|
||||||
|
SiRollupdotjs,
|
||||||
|
SiRuby,
|
||||||
|
SiSanity,
|
||||||
|
SiSass,
|
||||||
|
SiScala,
|
||||||
|
SiSentry,
|
||||||
|
SiShadcnui,
|
||||||
|
SiStorybook,
|
||||||
|
SiStylelint,
|
||||||
|
SiSublimetext,
|
||||||
|
SiSvelte,
|
||||||
|
SiSvg,
|
||||||
|
SiSwift,
|
||||||
|
SiTailwindcss,
|
||||||
|
SiToml,
|
||||||
|
SiTypescript,
|
||||||
|
SiVercel,
|
||||||
|
SiVite,
|
||||||
|
SiVuedotjs,
|
||||||
|
SiWebassembly,
|
||||||
|
} from '@icons-pack/react-simple-icons';
|
||||||
|
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
||||||
|
import { CheckIcon, CopyIcon } from 'lucide-react';
|
||||||
|
import type {
|
||||||
|
ComponentProps,
|
||||||
|
HTMLAttributes,
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
cloneElement,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export type BundledLanguage = string;
|
||||||
|
|
||||||
|
const filenameIconMap = {
|
||||||
|
'.env': SiDotenv,
|
||||||
|
'*.astro': SiAstro,
|
||||||
|
'biome.json': SiBiome,
|
||||||
|
'.bowerrc': SiBower,
|
||||||
|
'bun.lockb': SiBun,
|
||||||
|
'*.c': SiC,
|
||||||
|
'*.cpp': SiCplusplus,
|
||||||
|
'.circleci/config.yml': SiCircleci,
|
||||||
|
'*.coffee': SiCoffeescript,
|
||||||
|
'*.module.css': SiCssmodules,
|
||||||
|
'*.css': SiCss,
|
||||||
|
'*.dart': SiDart,
|
||||||
|
Dockerfile: SiDocker,
|
||||||
|
'docusaurus.config.js': SiDocusaurus,
|
||||||
|
'.editorconfig': SiEditorconfig,
|
||||||
|
'.eslintrc': SiEslint,
|
||||||
|
'eslint.config.*': SiEslint,
|
||||||
|
'gatsby-config.*': SiGatsby,
|
||||||
|
'.gitignore': SiGitignoredotio,
|
||||||
|
'*.go': SiGo,
|
||||||
|
'*.graphql': SiGraphql,
|
||||||
|
'*.sh': SiGnubash,
|
||||||
|
'Gruntfile.*': SiGrunt,
|
||||||
|
'gulpfile.*': SiGulp,
|
||||||
|
'*.hbs': SiHandlebarsdotjs,
|
||||||
|
'*.html': SiHtml5,
|
||||||
|
'*.js': SiJavascript,
|
||||||
|
'*.json': SiJson,
|
||||||
|
'*.test.js': SiJest,
|
||||||
|
'*.less': SiLess,
|
||||||
|
'*.md': SiMarkdown,
|
||||||
|
'*.mdx': SiMdx,
|
||||||
|
'mintlify.json': SiMintlify,
|
||||||
|
'mocha.opts': SiMocha,
|
||||||
|
'*.mustache': SiHandlebarsdotjs,
|
||||||
|
'*.sql': SiMysql,
|
||||||
|
'next.config.*': SiNextdotjs,
|
||||||
|
'*.pl': SiPerl,
|
||||||
|
'*.php': SiPhp,
|
||||||
|
'postcss.config.*': SiPostcss,
|
||||||
|
'prettier.config.*': SiPrettier,
|
||||||
|
'*.prisma': SiPrisma,
|
||||||
|
'*.pug': SiPug,
|
||||||
|
'*.py': SiPython,
|
||||||
|
'*.r': SiR,
|
||||||
|
'*.rb': SiRuby,
|
||||||
|
'*.jsx': SiReact,
|
||||||
|
'*.tsx': SiReact,
|
||||||
|
'readme.md': SiReadme,
|
||||||
|
'*.rdb': SiRedis,
|
||||||
|
'remix.config.*': SiRemix,
|
||||||
|
'*.riv': SiRive,
|
||||||
|
'rollup.config.*': SiRollupdotjs,
|
||||||
|
'sanity.config.*': SiSanity,
|
||||||
|
'*.sass': SiSass,
|
||||||
|
'*.scss': SiSass,
|
||||||
|
'*.sc': SiScala,
|
||||||
|
'*.scala': SiScala,
|
||||||
|
'sentry.client.config.*': SiSentry,
|
||||||
|
'components.json': SiShadcnui,
|
||||||
|
'storybook.config.*': SiStorybook,
|
||||||
|
'stylelint.config.*': SiStylelint,
|
||||||
|
'.sublime-settings': SiSublimetext,
|
||||||
|
'*.svelte': SiSvelte,
|
||||||
|
'*.svg': SiSvg,
|
||||||
|
'*.swift': SiSwift,
|
||||||
|
'tailwind.config.*': SiTailwindcss,
|
||||||
|
'*.toml': SiToml,
|
||||||
|
'*.ts': SiTypescript,
|
||||||
|
'vercel.json': SiVercel,
|
||||||
|
'vite.config.*': SiVite,
|
||||||
|
'*.vue': SiVuedotjs,
|
||||||
|
'*.wasm': SiWebassembly,
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineNumberClassNames = cn(
|
||||||
|
'[&_code]:[counter-reset:line]',
|
||||||
|
'[&_code]:[counter-increment:line_0]',
|
||||||
|
'[&_.line]:before:content-[counter(line)]',
|
||||||
|
'[&_.line]:before:inline-block',
|
||||||
|
'[&_.line]:before:[counter-increment:line]',
|
||||||
|
'[&_.line]:before:w-4',
|
||||||
|
'[&_.line]:before:mr-4',
|
||||||
|
'[&_.line]:before:text-[13px]',
|
||||||
|
'[&_.line]:before:text-right',
|
||||||
|
'[&_.line]:before:text-muted-foreground/50',
|
||||||
|
'[&_.line]:before:font-mono',
|
||||||
|
'[&_.line]:before:select-none'
|
||||||
|
);
|
||||||
|
|
||||||
|
const darkModeClassNames = cn(
|
||||||
|
'dark:[&_.shiki]:!text-[var(--shiki-dark)]',
|
||||||
|
'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]',
|
||||||
|
'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]',
|
||||||
|
'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]',
|
||||||
|
'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]',
|
||||||
|
'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]',
|
||||||
|
'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]',
|
||||||
|
'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]',
|
||||||
|
'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]'
|
||||||
|
);
|
||||||
|
|
||||||
|
const lineHighlightClassNames = cn(
|
||||||
|
'[&_.line.highlighted]:bg-blue-50',
|
||||||
|
'[&_.line.highlighted]:after:bg-blue-500',
|
||||||
|
'[&_.line.highlighted]:after:absolute',
|
||||||
|
'[&_.line.highlighted]:after:left-0',
|
||||||
|
'[&_.line.highlighted]:after:top-0',
|
||||||
|
'[&_.line.highlighted]:after:bottom-0',
|
||||||
|
'[&_.line.highlighted]:after:w-0.5',
|
||||||
|
'dark:[&_.line.highlighted]:!bg-blue-500/10'
|
||||||
|
);
|
||||||
|
|
||||||
|
const lineDiffClassNames = cn(
|
||||||
|
'[&_.line.diff]:after:absolute',
|
||||||
|
'[&_.line.diff]:after:left-0',
|
||||||
|
'[&_.line.diff]:after:top-0',
|
||||||
|
'[&_.line.diff]:after:bottom-0',
|
||||||
|
'[&_.line.diff]:after:w-0.5',
|
||||||
|
'[&_.line.diff.add]:bg-emerald-50',
|
||||||
|
'[&_.line.diff.add]:after:bg-emerald-500',
|
||||||
|
'[&_.line.diff.remove]:bg-rose-50',
|
||||||
|
'[&_.line.diff.remove]:after:bg-rose-500',
|
||||||
|
'dark:[&_.line.diff.add]:!bg-emerald-500/10',
|
||||||
|
'dark:[&_.line.diff.remove]:!bg-rose-500/10'
|
||||||
|
);
|
||||||
|
|
||||||
|
const lineFocusedClassNames = cn(
|
||||||
|
'[&_code:has(.focused)_.line]:blur-[2px]',
|
||||||
|
'[&_code:has(.focused)_.line.focused]:blur-none'
|
||||||
|
);
|
||||||
|
|
||||||
|
const wordHighlightClassNames = cn(
|
||||||
|
'[&_.highlighted-word]:bg-blue-50',
|
||||||
|
'dark:[&_.highlighted-word]:!bg-blue-500/10'
|
||||||
|
);
|
||||||
|
|
||||||
|
const codeBlockClassName = cn(
|
||||||
|
'mt-0 bg-background text-sm',
|
||||||
|
'[&_pre]:py-4',
|
||||||
|
'[&_.shiki]:!bg-[var(--shiki-bg)]',
|
||||||
|
'[&_code]:w-full',
|
||||||
|
'[&_code]:grid',
|
||||||
|
'[&_code]:overflow-x-auto',
|
||||||
|
'[&_code]:bg-transparent',
|
||||||
|
'[&_.line]:px-4',
|
||||||
|
'[&_.line]:w-full',
|
||||||
|
'[&_.line]:relative'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
type CodeBlockData = {
|
||||||
|
language: string;
|
||||||
|
filename: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CodeBlockContextType = {
|
||||||
|
value: string | undefined;
|
||||||
|
onValueChange: ((value: string) => void) | undefined;
|
||||||
|
data: CodeBlockData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const CodeBlockContext = createContext<CodeBlockContextType>({
|
||||||
|
value: undefined,
|
||||||
|
onValueChange: undefined,
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
defaultValue?: string;
|
||||||
|
value?: string;
|
||||||
|
onValueChange?: (value: string) => void;
|
||||||
|
data: CodeBlockData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlock = ({
|
||||||
|
value: controlledValue,
|
||||||
|
onValueChange: controlledOnValueChange,
|
||||||
|
defaultValue,
|
||||||
|
className,
|
||||||
|
data,
|
||||||
|
...props
|
||||||
|
}: CodeBlockProps) => {
|
||||||
|
const [value, onValueChange] = useControllableState({
|
||||||
|
defaultProp: defaultValue ?? '',
|
||||||
|
prop: controlledValue,
|
||||||
|
onChange: controlledOnValueChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CodeBlockContext.Provider value={{ value, onValueChange, data }}>
|
||||||
|
<div
|
||||||
|
className={cn('size-full overflow-hidden rounded-md border', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</CodeBlockContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const CodeBlockHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockHeaderProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-row items-center border-b bg-secondary p-1',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockFilesProps = Omit<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
'children'
|
||||||
|
> & {
|
||||||
|
children: (item: CodeBlockData) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockFiles = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CodeBlockFilesProps) => {
|
||||||
|
const { data } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('flex grow flex-row items-center gap-2', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{data.map(children)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
icon?: IconType;
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockFilename = ({
|
||||||
|
className,
|
||||||
|
icon,
|
||||||
|
value,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CodeBlockFilenameProps) => {
|
||||||
|
const { value: activeValue } = useContext(CodeBlockContext);
|
||||||
|
const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => {
|
||||||
|
const regex = new RegExp(
|
||||||
|
`^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$`
|
||||||
|
);
|
||||||
|
return regex.test(children as string);
|
||||||
|
})?.[1];
|
||||||
|
const Icon = icon ?? defaultIcon;
|
||||||
|
|
||||||
|
if (value !== activeValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{Icon && <Icon className="h-4 w-4 shrink-0" />}
|
||||||
|
<span className="flex-1 truncate">{children}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockSelectProps = ComponentProps<typeof Select>;
|
||||||
|
|
||||||
|
export const CodeBlockSelect = (props: CodeBlockSelectProps) => {
|
||||||
|
const { value, onValueChange } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
return <Select onValueChange={onValueChange} value={value} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;
|
||||||
|
|
||||||
|
export const CodeBlockSelectTrigger = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockSelectTriggerProps) => (
|
||||||
|
<SelectTrigger
|
||||||
|
className={cn(
|
||||||
|
'w-fit border-none text-muted-foreground text-xs shadow-none',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;
|
||||||
|
|
||||||
|
export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => (
|
||||||
|
<SelectValue {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockSelectContentProps = Omit<
|
||||||
|
ComponentProps<typeof SelectContent>,
|
||||||
|
'children'
|
||||||
|
> & {
|
||||||
|
children: (item: CodeBlockData) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockSelectContent = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CodeBlockSelectContentProps) => {
|
||||||
|
const { data } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
return <SelectContent {...props}>{data.map(children)}</SelectContent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;
|
||||||
|
|
||||||
|
export const CodeBlockSelectItem = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockSelectItemProps) => (
|
||||||
|
<SelectItem className={cn('text-sm', className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
||||||
|
onCopy?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockCopyButton = ({
|
||||||
|
asChild,
|
||||||
|
onCopy,
|
||||||
|
onError,
|
||||||
|
timeout = 2000,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockCopyButtonProps) => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const { data, value } = useContext(CodeBlockContext);
|
||||||
|
const code = data.find((item) => item.language === value)?.code;
|
||||||
|
|
||||||
|
const copyToClipboard = () => {
|
||||||
|
if (
|
||||||
|
typeof window === 'undefined' ||
|
||||||
|
!navigator.clipboard.writeText ||
|
||||||
|
!code
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(code).then(() => {
|
||||||
|
setIsCopied(true);
|
||||||
|
onCopy?.();
|
||||||
|
|
||||||
|
setTimeout(() => setIsCopied(false), timeout);
|
||||||
|
}, onError);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (asChild) {
|
||||||
|
return cloneElement(children as ReactElement, {
|
||||||
|
// @ts-expect-error - we know this is a button
|
||||||
|
onClick: copyToClipboard,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn('shrink-0', className)}
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <Icon className="text-muted-foreground" size={14} />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => (
|
||||||
|
<div {...props}>
|
||||||
|
<pre className="w-full">
|
||||||
|
<code>
|
||||||
|
{children
|
||||||
|
?.toString()
|
||||||
|
.split('\n')
|
||||||
|
.map((line, i) => (
|
||||||
|
<span className="line" key={i}>
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockBodyProps = Omit<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
'children'
|
||||||
|
> & {
|
||||||
|
children: (item: CodeBlockData) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => {
|
||||||
|
const { data } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
return <div {...props}>{data.map(children)}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
value: string;
|
||||||
|
lineNumbers?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockItem = ({
|
||||||
|
children,
|
||||||
|
lineNumbers = true,
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: CodeBlockItemProps) => {
|
||||||
|
const { value: activeValue } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
if (value !== activeValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
codeBlockClassName,
|
||||||
|
lineHighlightClassNames,
|
||||||
|
lineDiffClassNames,
|
||||||
|
lineFocusedClassNames,
|
||||||
|
wordHighlightClassNames,
|
||||||
|
darkModeClassNames,
|
||||||
|
lineNumbers && lineNumberClassNames,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
themes?: {
|
||||||
|
light: string;
|
||||||
|
dark: string;
|
||||||
|
};
|
||||||
|
language?: BundledLanguage;
|
||||||
|
syntaxHighlighting?: boolean;
|
||||||
|
children: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockContent = ({
|
||||||
|
children,
|
||||||
|
themes = {
|
||||||
|
light: 'vitesse-light',
|
||||||
|
dark: 'vitesse-dark',
|
||||||
|
},
|
||||||
|
language = 'typescript',
|
||||||
|
syntaxHighlighting = true,
|
||||||
|
...props
|
||||||
|
}: CodeBlockContentProps) => {
|
||||||
|
const [highlightedCode, setHighlightedCode] = useState<string>('');
|
||||||
|
const [isLoading, setIsLoading] = useState(syntaxHighlighting);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!syntaxHighlighting) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadHighlightedCode = async () => {
|
||||||
|
try {
|
||||||
|
const { codeToHtml } = await import('shiki');
|
||||||
|
|
||||||
|
const html = await codeToHtml(children, {
|
||||||
|
lang: language,
|
||||||
|
themes: {
|
||||||
|
light: themes.light,
|
||||||
|
dark: themes.dark,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setHighlightedCode(html);
|
||||||
|
setIsLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to highlight code for language "${language}":`, error);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadHighlightedCode();
|
||||||
|
}, [children, language, themes, syntaxHighlighting]);
|
||||||
|
|
||||||
|
if (!syntaxHighlighting || isLoading) {
|
||||||
|
return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
transformerNotationDiff,
|
||||||
|
transformerNotationErrorLevel,
|
||||||
|
transformerNotationFocus,
|
||||||
|
transformerNotationHighlight,
|
||||||
|
transformerNotationWordHighlight,
|
||||||
|
} from '@shikijs/transformers';
|
||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
import {
|
||||||
|
type BundledLanguage,
|
||||||
|
type CodeOptionsMultipleThemes,
|
||||||
|
codeToHtml,
|
||||||
|
} from 'shiki';
|
||||||
|
|
||||||
|
export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
themes?: CodeOptionsMultipleThemes['themes'];
|
||||||
|
language?: BundledLanguage;
|
||||||
|
children: string;
|
||||||
|
syntaxHighlighting?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockContent = async ({
|
||||||
|
children,
|
||||||
|
themes,
|
||||||
|
language,
|
||||||
|
syntaxHighlighting = true,
|
||||||
|
...props
|
||||||
|
}: CodeBlockContentProps) => {
|
||||||
|
const html = syntaxHighlighting
|
||||||
|
? await codeToHtml(children as string, {
|
||||||
|
lang: language ?? 'typescript',
|
||||||
|
themes: themes ?? {
|
||||||
|
light: 'vitesse-light',
|
||||||
|
dark: 'vitesse-dark',
|
||||||
|
},
|
||||||
|
transformers: [
|
||||||
|
transformerNotationDiff({
|
||||||
|
matchAlgorithm: 'v3',
|
||||||
|
}),
|
||||||
|
transformerNotationHighlight({
|
||||||
|
matchAlgorithm: 'v3',
|
||||||
|
}),
|
||||||
|
transformerNotationWordHighlight({
|
||||||
|
matchAlgorithm: 'v3',
|
||||||
|
}),
|
||||||
|
transformerNotationFocus({
|
||||||
|
matchAlgorithm: 'v3',
|
||||||
|
}),
|
||||||
|
transformerNotationErrorLevel({
|
||||||
|
matchAlgorithm: 'v3',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
: children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
// biome-ignore lint/security/noDangerouslySetInnerHtml: "Kinda how Shiki works"
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
45
inspiration-repo-agent/.gitignore
vendored
Normal file
45
inspiration-repo-agent/.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# OpenNext
|
||||||
|
/.open-next
|
||||||
|
|
||||||
|
# wrangler files
|
||||||
|
.wrangler
|
||||||
|
.dev.vars*
|
||||||
|
!.dev.vars.example
|
||||||
|
!.env.example
|
||||||
5
inspiration-repo-agent/.vscode/settings.json
vendored
Normal file
5
inspiration-repo-agent/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"wrangler.json": "jsonc"
|
||||||
|
}
|
||||||
|
}
|
||||||
36
inspiration-repo-agent/README.md
Normal file
36
inspiration-repo-agent/README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
144
inspiration-repo-agent/app_root_backup/globals.css
Normal file
144
inspiration-repo-agent/app_root_backup/globals.css
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.15 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.145 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.68 0.19 45);
|
||||||
|
--primary-foreground: oklch(0 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.396 0.141 25.723);
|
||||||
|
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||||
|
--border: oklch(0.269 0 0);
|
||||||
|
--input: oklch(0.269 0 0);
|
||||||
|
--ring: oklch(0.68 0.19 45);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(0.269 0 0);
|
||||||
|
--sidebar-ring: oklch(0.439 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
/* optional: --font-sans, --font-serif, --font-mono if they are applied in the layout.tsx */
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #ff8c00 #000000;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-track {
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background: #ff8c00;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #ff9d1a;
|
||||||
|
}
|
||||||
|
}
|
||||||
8342
inspiration-repo-agent/cloudflare-env.d.ts
vendored
Normal file
8342
inspiration-repo-agent/cloudflare-env.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
22
inspiration-repo-agent/components.json
Normal file
22
inspiration-repo-agent/components.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
16
inspiration-repo-agent/eslint.config.mjs
Normal file
16
inspiration-repo-agent/eslint.config.mjs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eslintConfig = [
|
||||||
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
11
inspiration-repo-agent/next.config.ts
Normal file
11
inspiration-repo-agent/next.config.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
|
|
||||||
|
// added by create cloudflare to enable calling `getCloudflareContext()` in `next dev`
|
||||||
|
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
|
||||||
|
initOpenNextCloudflareForDev();
|
||||||
9
inspiration-repo-agent/open-next.config.ts
Normal file
9
inspiration-repo-agent/open-next.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({
|
||||||
|
// Uncomment to enable R2 cache,
|
||||||
|
// It should be imported as:
|
||||||
|
// `import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";`
|
||||||
|
// See https://opennext.js.org/cloudflare/caching for more details
|
||||||
|
// incrementalCache: r2IncrementalCache,
|
||||||
|
});
|
||||||
18612
inspiration-repo-agent/package-lock.json
generated
Normal file
18612
inspiration-repo-agent/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
74
inspiration-repo-agent/package.json
Normal file
74
inspiration-repo-agent/package.json
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "my-v0-project",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.10.0",
|
||||||
|
"@radix-ui/react-accordion": "1.2.2",
|
||||||
|
"@radix-ui/react-alert-dialog": "1.1.4",
|
||||||
|
"@radix-ui/react-aspect-ratio": "1.1.1",
|
||||||
|
"@radix-ui/react-avatar": "1.1.2",
|
||||||
|
"@radix-ui/react-checkbox": "1.1.3",
|
||||||
|
"@radix-ui/react-collapsible": "1.1.2",
|
||||||
|
"@radix-ui/react-context-menu": "2.2.4",
|
||||||
|
"@radix-ui/react-dialog": "1.1.4",
|
||||||
|
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||||
|
"@radix-ui/react-hover-card": "1.1.4",
|
||||||
|
"@radix-ui/react-label": "2.1.1",
|
||||||
|
"@radix-ui/react-menubar": "1.1.4",
|
||||||
|
"@radix-ui/react-navigation-menu": "1.2.3",
|
||||||
|
"@radix-ui/react-popover": "1.1.4",
|
||||||
|
"@radix-ui/react-progress": "1.1.1",
|
||||||
|
"@radix-ui/react-radio-group": "1.2.2",
|
||||||
|
"@radix-ui/react-scroll-area": "1.2.2",
|
||||||
|
"@radix-ui/react-select": "2.1.4",
|
||||||
|
"@radix-ui/react-separator": "1.1.1",
|
||||||
|
"@radix-ui/react-slider": "1.2.2",
|
||||||
|
"@radix-ui/react-slot": "1.1.1",
|
||||||
|
"@radix-ui/react-switch": "1.1.2",
|
||||||
|
"@radix-ui/react-tabs": "1.1.2",
|
||||||
|
"@radix-ui/react-toast": "1.2.4",
|
||||||
|
"@radix-ui/react-toggle": "1.1.1",
|
||||||
|
"@radix-ui/react-toggle-group": "1.1.1",
|
||||||
|
"@radix-ui/react-tooltip": "1.1.6",
|
||||||
|
"@vercel/analytics": "1.3.1",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "1.0.4",
|
||||||
|
"date-fns": "4.1.0",
|
||||||
|
"embla-carousel-react": "8.5.1",
|
||||||
|
"geist": "^1.3.1",
|
||||||
|
"input-otp": "1.4.1",
|
||||||
|
"lucide-react": "^0.454.0",
|
||||||
|
"next": "15.5.4",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-day-picker": "9.8.0",
|
||||||
|
"react-dom": "19.1.0",
|
||||||
|
"react-hook-form": "^7.60.0",
|
||||||
|
"react-resizable-panels": "^2.1.7",
|
||||||
|
"recharts": "2.15.4",
|
||||||
|
"sonner": "^1.7.4",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^0.9.9",
|
||||||
|
"zod": "3.25.67"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.9",
|
||||||
|
"@types/node": "^22",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"postcss": "^8.5",
|
||||||
|
"tailwindcss": "^4.1.9",
|
||||||
|
"tw-animate-css": "1.3.3",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
inspiration-repo-agent/postcss.config.mjs
Normal file
5
inspiration-repo-agent/postcss.config.mjs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: ["@tailwindcss/postcss"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
3
inspiration-repo-agent/public/_headers
Normal file
3
inspiration-repo-agent/public/_headers
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://developers.cloudflare.com/workers/static-assets/headers
|
||||||
|
/_next/static/*
|
||||||
|
Cache-Control: public,max-age=31536000,immutable
|
||||||
1
inspiration-repo-agent/public/file.svg
Normal file
1
inspiration-repo-agent/public/file.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
inspiration-repo-agent/public/globe.svg
Normal file
1
inspiration-repo-agent/public/globe.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
inspiration-repo-agent/public/next.svg
Normal file
1
inspiration-repo-agent/public/next.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
inspiration-repo-agent/public/vercel.svg
Normal file
1
inspiration-repo-agent/public/vercel.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
1
inspiration-repo-agent/public/window.svg
Normal file
1
inspiration-repo-agent/public/window.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
118
inspiration-repo-agent/src/app/api/chat/route.ts
Normal file
118
inspiration-repo-agent/src/app/api/chat/route.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { type NextRequest, NextResponse } from "next/server"
|
||||||
|
|
||||||
|
const WEBHOOK_URL = "https://n8n.biohazardvfx.com/webhook/d2ab4653-a107-412c-a905-ccd80e5b76cd"
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { message, timestamp, sessionId } = body
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return NextResponse.json({ error: "Message is required" }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[v0] Sending to webhook:", { message, timestamp, sessionId })
|
||||||
|
|
||||||
|
const response = await fetch(WEBHOOK_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message,
|
||||||
|
timestamp,
|
||||||
|
sessionId,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("[v0] Webhook response status:", response.status)
|
||||||
|
|
||||||
|
const responseText = await response.text()
|
||||||
|
console.log("[v0] Webhook response body (first 200 chars):", responseText.substring(0, 200))
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// Try to parse as JSON if possible, otherwise use text
|
||||||
|
let errorData
|
||||||
|
try {
|
||||||
|
errorData = responseText ? JSON.parse(responseText) : {}
|
||||||
|
} catch {
|
||||||
|
errorData = { message: responseText || "Unknown error" }
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("[v0] Webhook error:", errorData)
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: errorData.message || "Failed to communicate with webhook",
|
||||||
|
hint: errorData.hint,
|
||||||
|
code: errorData.code,
|
||||||
|
},
|
||||||
|
{ status: response.status },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseText) {
|
||||||
|
console.log("[v0] Empty response from webhook")
|
||||||
|
return NextResponse.json({
|
||||||
|
response:
|
||||||
|
"The webhook received your message but didn't return a response. Please ensure your n8n workflow includes a 'Respond to Webhook' node that returns data.",
|
||||||
|
hint: "Add a 'Respond to Webhook' node in your n8n workflow to send responses back to the chat.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Split response by newlines to get individual JSON objects
|
||||||
|
const lines = responseText.trim().split("\n")
|
||||||
|
const chunks: string[] = []
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chunk = JSON.parse(line)
|
||||||
|
|
||||||
|
// Extract content from "item" type chunks
|
||||||
|
if (chunk.type === "item" && chunk.content) {
|
||||||
|
chunks.push(chunk.content)
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.log("[v0] Failed to parse line:", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all chunks into a single message
|
||||||
|
if (chunks.length > 0) {
|
||||||
|
const fullMessage = chunks.join("")
|
||||||
|
console.log("[v0] Combined message from", chunks.length, "chunks")
|
||||||
|
return NextResponse.json({ response: fullMessage })
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no chunks found, try parsing as regular JSON
|
||||||
|
const data = JSON.parse(responseText)
|
||||||
|
console.log("[v0] Parsed webhook data:", data)
|
||||||
|
|
||||||
|
// Extract the response from various possible fields
|
||||||
|
let responseMessage = data.response || data.message || data.output || data.text
|
||||||
|
|
||||||
|
// If the response is an object, try to extract from nested fields
|
||||||
|
if (typeof responseMessage === "object") {
|
||||||
|
responseMessage =
|
||||||
|
responseMessage.response || responseMessage.message || responseMessage.output || responseMessage.text
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no message found, stringify the entire response
|
||||||
|
if (!responseMessage) {
|
||||||
|
responseMessage = JSON.stringify(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ response: responseMessage })
|
||||||
|
} catch (parseError) {
|
||||||
|
console.log("[v0] Response is not JSON, returning as text")
|
||||||
|
// If not JSON, return the text as the response
|
||||||
|
return NextResponse.json({ response: responseText })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[v0] API route error:", error)
|
||||||
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
inspiration-repo-agent/src/app/favicon.ico
Normal file
BIN
inspiration-repo-agent/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
145
inspiration-repo-agent/src/app/globals.css
Normal file
145
inspiration-repo-agent/src/app/globals.css
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.15 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.145 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.68 0.19 45);
|
||||||
|
--primary-foreground: oklch(0 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.396 0.141 25.723);
|
||||||
|
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||||
|
--border: oklch(0.269 0 0);
|
||||||
|
--input: oklch(0.269 0 0);
|
||||||
|
--ring: oklch(0.68 0.19 45);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(0.269 0 0);
|
||||||
|
--sidebar-ring: oklch(0.439 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
/* optional: --font-sans, --font-serif, --font-mono if they are applied in the layout.tsx */
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #ff8c00 #000000;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-track {
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background: #ff8c00;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #ff9d1a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
28
inspiration-repo-agent/src/app/layout.tsx
Normal file
28
inspiration-repo-agent/src/app/layout.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import type React from "react"
|
||||||
|
import type { Metadata } from "next"
|
||||||
|
import { GeistSans } from "geist/font/sans"
|
||||||
|
import { GeistMono } from "geist/font/mono"
|
||||||
|
import { Analytics } from "@vercel/analytics/next"
|
||||||
|
import { Suspense } from "react"
|
||||||
|
import "./globals.css"
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "AI Chat Interface",
|
||||||
|
description: "Chat with AI Agent",
|
||||||
|
generator: "v0.app",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className="dark">
|
||||||
|
<body className={`font-sans ${GeistSans.variable} ${GeistMono.variable}`}>
|
||||||
|
<Suspense fallback={null}>{children}</Suspense>
|
||||||
|
<Analytics />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
inspiration-repo-agent/src/app/page.tsx
Normal file
9
inspiration-repo-agent/src/app/page.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ChatInterface } from "@/components/chat-interface"
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main className="bg-black">
|
||||||
|
<ChatInterface />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
219
inspiration-repo-agent/src/components/chat-interface.tsx
Normal file
219
inspiration-repo-agent/src/components/chat-interface.tsx
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Send, Bot, Loader2, SquarePen } from "lucide-react"
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: string
|
||||||
|
role: "user" | "assistant"
|
||||||
|
content: string
|
||||||
|
timestamp: Date
|
||||||
|
isError?: boolean
|
||||||
|
hint?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatInterface() {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
|
const [input, setInput] = useState("")
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [sessionId, setSessionId] = useState<string>("")
|
||||||
|
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Try to get existing sessionID from localStorage
|
||||||
|
let existingSessionId = localStorage.getItem("chat-session-id")
|
||||||
|
|
||||||
|
if (!existingSessionId) {
|
||||||
|
// Generate new sessionID using timestamp and random string
|
||||||
|
existingSessionId = `session-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`
|
||||||
|
localStorage.setItem("chat-session-id", existingSessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionId(existingSessionId)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messagesContainerRef.current) {
|
||||||
|
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight
|
||||||
|
}
|
||||||
|
}, [messages, isLoading])
|
||||||
|
|
||||||
|
const sendMessage = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!input.trim() || isLoading) return
|
||||||
|
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
role: "user",
|
||||||
|
content: input.trim(),
|
||||||
|
timestamp: new Date(),
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, userMessage])
|
||||||
|
setInput("")
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/chat", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: userMessage.content,
|
||||||
|
timestamp: userMessage.timestamp.toISOString(),
|
||||||
|
sessionId: sessionId,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorMessage: Message = {
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
role: "assistant",
|
||||||
|
content: data.error || "Failed to communicate with the webhook.",
|
||||||
|
timestamp: new Date(),
|
||||||
|
isError: true,
|
||||||
|
hint: data.hint,
|
||||||
|
}
|
||||||
|
setMessages((prev) => [...prev, errorMessage])
|
||||||
|
} else {
|
||||||
|
const assistantMessage: Message = {
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
role: "assistant",
|
||||||
|
content: data.response || data.message || JSON.stringify(data),
|
||||||
|
timestamp: new Date(),
|
||||||
|
}
|
||||||
|
setMessages((prev) => [...prev, assistantMessage])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[v0] Error sending message:", error)
|
||||||
|
|
||||||
|
const errorMessage: Message = {
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
role: "assistant",
|
||||||
|
content: "Sorry, I encountered an error processing your message. Please try again.",
|
||||||
|
timestamp: new Date(),
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, errorMessage])
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
inputRef.current?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startNewChat = () => {
|
||||||
|
// Clear all messages
|
||||||
|
setMessages([])
|
||||||
|
// Generate new sessionID
|
||||||
|
const newSessionId = `session-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`
|
||||||
|
setSessionId(newSessionId)
|
||||||
|
localStorage.setItem("chat-session-id", newSessionId)
|
||||||
|
// Clear input
|
||||||
|
setInput("")
|
||||||
|
// Focus input
|
||||||
|
inputRef.current?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-full flex-col">
|
||||||
|
{messages.length > 0 && (
|
||||||
|
<div className="absolute right-4 top-4 z-10">
|
||||||
|
<Button
|
||||||
|
onClick={startNewChat}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 rounded-full bg-neutral-900/80 text-neutral-400 backdrop-blur-sm hover:bg-neutral-800 hover:text-white"
|
||||||
|
title="Start new chat"
|
||||||
|
>
|
||||||
|
<SquarePen className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto px-4 py-8 pb-32">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="flex h-full min-h-[60vh] flex-col items-center justify-center gap-3 text-center">
|
||||||
|
<h1 className="text-2xl font-semibold text-white">Hello there!</h1>
|
||||||
|
<p className="text-base text-neutral-400">How can I help you today?</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<div key={message.id} className="flex gap-3">
|
||||||
|
{message.role === "assistant" && (
|
||||||
|
<div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-neutral-800">
|
||||||
|
<Bot className="h-4 w-4 text-neutral-400" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={`flex-1 ${message.role === "user" ? "flex justify-end" : ""}`}>
|
||||||
|
<div
|
||||||
|
className={`inline-block max-w-[85%] rounded-2xl px-4 py-2.5 ${
|
||||||
|
message.role === "user"
|
||||||
|
? "bg-primary text-white"
|
||||||
|
: message.isError
|
||||||
|
? "bg-red-500/10 text-red-400"
|
||||||
|
: "bg-neutral-800/50 text-neutral-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="whitespace-pre-wrap break-words text-sm leading-relaxed">{message.content}</p>
|
||||||
|
</div>
|
||||||
|
{message.hint && <p className="mt-1 text-xs text-neutral-500">{message.hint}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-neutral-800">
|
||||||
|
<Bot className="h-4 w-4 text-neutral-400" />
|
||||||
|
</div>
|
||||||
|
<div className="inline-block rounded-2xl bg-neutral-800/50 px-4 py-2.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-neutral-400" />
|
||||||
|
<p className="text-sm text-neutral-400">Thinking...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="fixed bottom-6 left-1/2 z-20 w-full max-w-3xl -translate-x-1/2 px-4">
|
||||||
|
<form onSubmit={sendMessage} className="relative">
|
||||||
|
<div className="flex items-center gap-2 rounded-2xl border border-neutral-800 bg-neutral-900/95 p-2 shadow-2xl backdrop-blur-md">
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
placeholder="Ask a follow-up..."
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex-1 border-0 bg-transparent text-white placeholder:text-neutral-500 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!input.trim() || isLoading}
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 flex-shrink-0 rounded-xl bg-primary text-white hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
60
inspiration-repo-agent/src/components/ui/button.tsx
Normal file
60
inspiration-repo-agent/src/components/ui/button.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
21
inspiration-repo-agent/src/components/ui/input.tsx
Normal file
21
inspiration-repo-agent/src/components/ui/input.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
6
inspiration-repo-agent/src/lib/utils.ts
Normal file
6
inspiration-repo-agent/src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
31
inspiration-repo-agent/tsconfig.json
Normal file
31
inspiration-repo-agent/tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
"./cloudflare-env.d.ts",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
51
inspiration-repo-agent/wrangler.jsonc
Normal file
51
inspiration-repo-agent/wrangler.jsonc
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* For more details on how to configure Wrangler, refer to:
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "inspiration-repo-agent",
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"compatibility_date": "2025-03-01",
|
||||||
|
"compatibility_flags": [
|
||||||
|
"nodejs_compat",
|
||||||
|
"global_fetch_strictly_public"
|
||||||
|
],
|
||||||
|
"assets": {
|
||||||
|
"binding": "ASSETS",
|
||||||
|
"directory": ".open-next/assets"
|
||||||
|
},
|
||||||
|
"observability": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Smart Placement
|
||||||
|
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||||
|
*/
|
||||||
|
// "placement": { "mode": "smart" }
|
||||||
|
/**
|
||||||
|
* Bindings
|
||||||
|
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||||
|
* databases, object storage, AI inference, real-time communication and more.
|
||||||
|
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Environment Variables
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||||
|
*/
|
||||||
|
// "vars": { "MY_VARIABLE": "production_value" }
|
||||||
|
/**
|
||||||
|
* Note: Use secrets to store sensitive data.
|
||||||
|
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Static Assets
|
||||||
|
* https://developers.cloudflare.com/workers/static-assets/binding/
|
||||||
|
*/
|
||||||
|
// "assets": { "directory": "./public/", "binding": "ASSETS" }
|
||||||
|
/**
|
||||||
|
* Service Bindings (communicate between multiple Workers)
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
||||||
|
*/
|
||||||
|
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user