87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/src/app/sign-in/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 | import { useRouter } from 'next/navigation';
5 | import Link from 'next/link';
6 | import { signIn } from '@/lib/auth-client';
7 | import { Button } from '@/components/ui/button';
8 | import { Input } from '@/components/ui/input';
9 | import { Label } from '@/components/ui/label';
10 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
11 | import { LogIn } from 'lucide-react';
12 |
13 | export default function SignInPage() {
14 | const [email, setEmail] = useState('');
15 | const [password, setPassword] = useState('');
16 | const [error, setError] = useState('');
17 | const [loading, setLoading] = useState(false);
18 | const router = useRouter();
19 |
20 | const handleSubmit = async (e: React.FormEvent) => {
21 | e.preventDefault();
22 | setError('');
23 | setLoading(true);
24 |
25 | try {
26 | await signIn.email({
27 | email,
28 | password,
29 | });
30 | router.push('/');
31 | router.refresh();
32 | } catch (err) {
33 | setError('Invalid email or password');
34 | } finally {
35 | setLoading(false);
36 | }
37 | };
38 |
39 | return (
40 |
41 |
42 |
43 |
48 | Sign in
49 |
50 | Enter your email and password to access your account
51 |
52 |
53 |
54 |
87 |
88 | Don't have an account?{' '}
89 |
90 | Sign up
91 |
92 |
93 |
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/app/admin/users/user-management-table.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 | import {
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableHeader,
10 | TableRow,
11 | } from '@/components/ui/table';
12 | import { Button } from '@/components/ui/button';
13 | import { Badge } from '@/components/ui/badge';
14 | import {
15 | DropdownMenu,
16 | DropdownMenuContent,
17 | DropdownMenuItem,
18 | DropdownMenuTrigger,
19 | } from '@/components/ui/dropdown-menu';
20 | import { MoreVertical, Shield, User } from 'lucide-react';
21 | import { updateUserRole } from './actions';
22 |
23 | type User = {
24 | id: string;
25 | name: string | null;
26 | email: string;
27 | role: string;
28 | emailVerified: boolean;
29 | createdAt: Date;
30 | };
31 |
32 | export function UserManagementTable({ users }: { users: User[] }) {
33 | const [loading, setLoading] = useState(null);
34 |
35 | const handleRoleChange = async (userId: string, newRole: 'USER' | 'ADMIN') => {
36 | setLoading(userId);
37 | try {
38 | await updateUserRole(userId, newRole);
39 | } catch (error) {
40 | console.error('Failed to update role:', error);
41 | } finally {
42 | setLoading(null);
43 | }
44 | };
45 |
46 | return (
47 |
48 |
49 |
50 |
51 | Name
52 | Email
53 | Role
54 | Status
55 | Joined
56 |
57 |
58 |
59 |
60 | {users.map((user) => (
61 |
62 | {user.name || 'N/A'}
63 | {user.email}
64 |
65 |
66 | {user.role === 'ADMIN' ? (
67 |
68 | ) : (
69 |
70 | )}
71 | {user.role}
72 |
73 |
74 |
75 | {user.emailVerified ? (
76 | Verified
77 | ) : (
78 |
79 | Unverified
80 |
81 | )}
82 |
83 | {new Date(user.createdAt).toLocaleDateString()}
84 |
85 |
86 |
87 |
93 |
94 |
95 |
96 |
97 | {user.role === 'USER' ? (
98 | handleRoleChange(user.id, 'ADMIN')}
100 | >
101 |
102 | Make Admin
103 |
104 | ) : (
105 | handleRoleChange(user.id, 'USER')}
107 | >
108 |
109 | Remove Admin
110 |
111 | )}
112 |
113 |
114 |
115 |
116 | ))}
117 |
118 |
119 |
120 | );
121 | }
122 |
--------------------------------------------------------------------------------
/src/components/ui/shadcn-io/ai/code-block.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Button } from '@/components/ui/button';
4 | import { cn } from '@/lib/utils';
5 | import { CheckIcon, CopyIcon } from 'lucide-react';
6 | import type { ComponentProps, HTMLAttributes, ReactNode } from 'react';
7 | import { createContext, useContext, useState } from 'react';
8 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
9 | import {
10 | oneDark,
11 | oneLight,
12 | } from 'react-syntax-highlighter/dist/esm/styles/prism';
13 |
14 | type CodeBlockContextType = {
15 | code: string;
16 | };
17 |
18 | const CodeBlockContext = createContext({
19 | code: '',
20 | });
21 |
22 | export type CodeBlockProps = HTMLAttributes & {
23 | code: string;
24 | language: string;
25 | showLineNumbers?: boolean;
26 | children?: ReactNode;
27 | };
28 |
29 | export const CodeBlock = ({
30 | code,
31 | language,
32 | showLineNumbers = false,
33 | className,
34 | children,
35 | ...props
36 | }: CodeBlockProps) => (
37 |
38 |
45 |
46 |
67 | {code}
68 |
69 |
90 | {code}
91 |
92 | {children && (
93 |
94 | {children}
95 |
96 | )}
97 |
98 |
99 |
100 | );
101 |
102 | export type CodeBlockCopyButtonProps = ComponentProps & {
103 | onCopy?: () => void;
104 | onError?: (error: Error) => void;
105 | timeout?: number;
106 | };
107 |
108 | export const CodeBlockCopyButton = ({
109 | onCopy,
110 | onError,
111 | timeout = 2000,
112 | children,
113 | className,
114 | ...props
115 | }: CodeBlockCopyButtonProps) => {
116 | const [isCopied, setIsCopied] = useState(false);
117 | const { code } = useContext(CodeBlockContext);
118 |
119 | const copyToClipboard = async () => {
120 | if (typeof window === 'undefined' || !navigator.clipboard.writeText) {
121 | onError?.(new Error('Clipboard API not available'));
122 | return;
123 | }
124 |
125 | try {
126 | await navigator.clipboard.writeText(code);
127 | setIsCopied(true);
128 | onCopy?.();
129 | setTimeout(() => setIsCopied(false), timeout);
130 | } catch (error) {
131 | onError?.(error as Error);
132 | }
133 | };
134 |
135 | const Icon = isCopied ? CheckIcon : CopyIcon;
136 |
137 | return (
138 |
145 | {children ?? }
146 |
147 | );
148 | };
149 |
--------------------------------------------------------------------------------
/src/components/ui/shadcn-io/ai/tool.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Badge } from '@/components/ui/badge';
4 | import {
5 | Collapsible,
6 | CollapsibleContent,
7 | CollapsibleTrigger,
8 | } from '@/components/ui/collapsible';
9 | import { cn } from '@/lib/utils';
10 | import type { ToolUIPart } from 'ai';
11 | import {
12 | CheckCircleIcon,
13 | ChevronDownIcon,
14 | CircleIcon,
15 | ClockIcon,
16 | WrenchIcon,
17 | XCircleIcon,
18 | } from 'lucide-react';
19 | import type { ComponentProps, ReactNode } from 'react';
20 | import { CodeBlock } from './code-block';
21 |
22 | export type ToolProps = ComponentProps;
23 |
24 | export const Tool = ({ className, ...props }: ToolProps) => (
25 |
29 | );
30 |
31 | export type ToolHeaderProps = {
32 | type: ToolUIPart['type'];
33 | state: ToolUIPart['state'];
34 | className?: string;
35 | };
36 |
37 | const getStatusBadge = (status: ToolUIPart['state']) => {
38 | const labels = {
39 | 'input-streaming': 'Pending',
40 | 'input-available': 'Running',
41 | 'output-available': 'Completed',
42 | 'output-error': 'Error',
43 | } as const;
44 |
45 | const icons = {
46 | 'input-streaming': ,
47 | 'input-available': ,
48 | 'output-available': ,
49 | 'output-error': ,
50 | } as const;
51 |
52 | return (
53 |
54 | {icons[status]}
55 | {labels[status]}
56 |
57 | );
58 | };
59 |
60 | export const ToolHeader = ({
61 | className,
62 | type,
63 | state,
64 | ...props
65 | }: ToolHeaderProps) => (
66 |
73 |
74 |
75 | {type}
76 | {getStatusBadge(state)}
77 |
78 |
79 |
80 | );
81 |
82 | export type ToolContentProps = ComponentProps;
83 |
84 | export const ToolContent = ({ className, ...props }: ToolContentProps) => (
85 |
92 | );
93 |
94 | export type ToolInputProps = ComponentProps<'div'> & {
95 | input: ToolUIPart['input'];
96 | };
97 |
98 | export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
99 |
100 |
101 | Parameters
102 |
103 |
104 |
105 |
106 |
107 | );
108 |
109 | export type ToolOutputProps = ComponentProps<'div'> & {
110 | output: ReactNode;
111 | errorText: ToolUIPart['errorText'];
112 | };
113 |
114 | export const ToolOutput = ({
115 | className,
116 | output,
117 | errorText,
118 | ...props
119 | }: ToolOutputProps) => {
120 | if (!(output || errorText)) {
121 | return null;
122 | }
123 |
124 | return (
125 |
126 |
127 | {errorText ? 'Error' : 'Result'}
128 |
129 |
137 | {errorText &&
{errorText}
}
138 | {output &&
{output}
}
139 |
140 |
141 | );
142 | };
143 |
--------------------------------------------------------------------------------
/src/app/sign-up/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 | import { useRouter } from 'next/navigation';
5 | import Link from 'next/link';
6 | import { signUp } from '@/lib/auth-client';
7 | import { Button } from '@/components/ui/button';
8 | import { Input } from '@/components/ui/input';
9 | import { Label } from '@/components/ui/label';
10 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
11 | import { UserPlus } from 'lucide-react';
12 |
13 | export default function SignUpPage() {
14 | const [name, setName] = useState('');
15 | const [email, setEmail] = useState('');
16 | const [password, setPassword] = useState('');
17 | const [confirmPassword, setConfirmPassword] = useState('');
18 | const [error, setError] = useState('');
19 | const [loading, setLoading] = useState(false);
20 | const router = useRouter();
21 |
22 | const handleSubmit = async (e: React.FormEvent) => {
23 | e.preventDefault();
24 | setError('');
25 |
26 | if (password !== confirmPassword) {
27 | setError('Passwords do not match');
28 | return;
29 | }
30 |
31 | if (password.length < 8) {
32 | setError('Password must be at least 8 characters');
33 | return;
34 | }
35 |
36 | setLoading(true);
37 |
38 | try {
39 | await signUp.email({
40 | email,
41 | password,
42 | name,
43 | });
44 | router.push('/');
45 | router.refresh();
46 | } catch (err) {
47 | setError('Failed to create account. Email may already be in use.');
48 | } finally {
49 | setLoading(false);
50 | }
51 | };
52 |
53 | return (
54 |
55 |
56 |
57 |
62 | Create an account
63 |
64 | Enter your information to get started
65 |
66 |
67 |
68 |
124 |
125 | Already have an account?{' '}
126 |
127 | Sign in
128 |
129 |
130 |
131 |
132 |
133 | );
134 | }
135 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Link from 'next/link';
4 | import { useSession } from '@/lib/auth-client';
5 | import { Button } from '@/components/ui/button';
6 | import { Card } from '@/components/ui/card';
7 | import { ArrowRight, Zap, Shield, Users, Sparkles } from 'lucide-react';
8 |
9 | export default function Home() {
10 | const { data: session } = useSession();
11 |
12 | return (
13 |
14 | {/* Hero Section */}
15 |
16 |
17 |
18 | Build faster with our boilerplate
19 |
20 |
21 | A modern Next.js boilerplate with authentication, role-based access control,
22 | and admin console. Everything you need to start your next project.
23 |
24 |
25 |
26 |
27 |
28 | Try AI Chat
29 |
30 |
31 | {session ? (
32 | <>
33 | {session.user?.role === 'ADMIN' && (
34 |
35 |
36 | Admin Console
37 |
38 |
39 |
40 | )}
41 | >
42 | ) : (
43 | <>
44 |
45 |
46 | Get Started
47 |
48 |
49 |
50 | >
51 | )}
52 |
53 |
54 |
55 |
56 | {/* Features Section */}
57 |
58 |
59 |
Features
60 |
61 |
62 |
63 |
64 |
65 | AI Chat
66 |
67 | Built-in AI assistant powered by OpenAI with streaming responses and markdown support.
68 |
69 |
70 |
71 |
72 |
73 |
74 | Fast & Modern
75 |
76 | Built with Next.js 14, React 18, and modern best practices for optimal performance.
77 |
78 |
79 |
80 |
81 |
82 |
83 | Secure Auth
84 |
85 | Email/password authentication with better-auth and role-based access control.
86 |
87 |
88 |
89 |
90 |
91 |
92 | Admin Console
93 |
94 | Complete admin panel with user management and role assignment capabilities.
95 |
96 |
97 |
98 |
99 |
100 |
101 | {/* CTA Section */}
102 |
103 |
104 |
Ready to get started?
105 |
106 | Try our AI Chat or create an account to explore all features.
107 |
108 |
109 |
110 |
111 |
112 | Try AI Chat
113 |
114 |
115 | {!session && (
116 |
117 |
118 | Create Account
119 |
120 |
121 |
122 | )}
123 |
124 |
125 |
126 |
127 | );
128 | }
129 |
--------------------------------------------------------------------------------
/src/components/ui/shadcn-io/ai/reasoning.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useControllableState } from '@radix-ui/react-use-controllable-state';
4 | import {
5 | Collapsible,
6 | CollapsibleContent,
7 | CollapsibleTrigger,
8 | } from '@/components/ui/collapsible';
9 | import { cn } from '@/lib/utils';
10 | import { BrainIcon, ChevronDownIcon } from 'lucide-react';
11 | import type { ComponentProps } from 'react';
12 | import { createContext, memo, useContext, useEffect, useState } from 'react';
13 | import { Response } from './response';
14 |
15 | type ReasoningContextValue = {
16 | isStreaming: boolean;
17 | isOpen: boolean;
18 | setIsOpen: (open: boolean) => void;
19 | duration: number;
20 | };
21 |
22 | const ReasoningContext = createContext(null);
23 |
24 | const useReasoning = () => {
25 | const context = useContext(ReasoningContext);
26 | if (!context) {
27 | throw new Error('Reasoning components must be used within Reasoning');
28 | }
29 | return context;
30 | };
31 |
32 | export type ReasoningProps = ComponentProps & {
33 | isStreaming?: boolean;
34 | open?: boolean;
35 | defaultOpen?: boolean;
36 | onOpenChange?: (open: boolean) => void;
37 | duration?: number;
38 | };
39 |
40 | const AUTO_CLOSE_DELAY = 1000;
41 |
42 | export const Reasoning = memo(
43 | ({
44 | className,
45 | isStreaming = false,
46 | open,
47 | defaultOpen = false,
48 | onOpenChange,
49 | duration: durationProp,
50 | children,
51 | ...props
52 | }: ReasoningProps) => {
53 | const [isOpen, setIsOpen] = useControllableState({
54 | prop: open,
55 | defaultProp: defaultOpen,
56 | onChange: onOpenChange,
57 | });
58 | const [duration, setDuration] = useControllableState({
59 | prop: durationProp,
60 | defaultProp: 0,
61 | });
62 |
63 | const [hasAutoClosedRef, setHasAutoClosedRef] = useState(false);
64 | const [startTime, setStartTime] = useState(null);
65 |
66 | // Track duration when streaming starts and ends
67 | useEffect(() => {
68 | if (isStreaming) {
69 | if (startTime === null) {
70 | setStartTime(Date.now());
71 | }
72 | } else if (startTime !== null) {
73 | setDuration(Math.round((Date.now() - startTime) / 1000));
74 | setStartTime(null);
75 | }
76 | }, [isStreaming, startTime, setDuration]);
77 |
78 | // Auto-open when streaming starts, auto-close when streaming ends (once only)
79 | useEffect(() => {
80 | if (isStreaming && !isOpen) {
81 | setIsOpen(true);
82 | } else if (!isStreaming && isOpen && !defaultOpen && !hasAutoClosedRef) {
83 | // Add a small delay before closing to allow user to see the content
84 | const timer = setTimeout(() => {
85 | setIsOpen(false);
86 | setHasAutoClosedRef(true);
87 | }, AUTO_CLOSE_DELAY);
88 | return () => clearTimeout(timer);
89 | }
90 | }, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosedRef]);
91 |
92 | const handleOpenChange = (newOpen: boolean) => {
93 | setIsOpen(newOpen);
94 | };
95 |
96 | return (
97 |
100 |
106 | {children}
107 |
108 |
109 | );
110 | }
111 | );
112 |
113 | export type ReasoningTriggerProps = ComponentProps<
114 | typeof CollapsibleTrigger
115 | > & {
116 | title?: string;
117 | };
118 |
119 | export const ReasoningTrigger = memo(
120 | ({
121 | className,
122 | title = 'Reasoning',
123 | children,
124 | ...props
125 | }: ReasoningTriggerProps) => {
126 | const { isStreaming, isOpen, duration } = useReasoning();
127 |
128 | return (
129 |
136 | {children ?? (
137 | <>
138 |
139 | {isStreaming || duration === 0 ? (
140 | Thinking...
141 | ) : (
142 | Thought for {duration} seconds
143 | )}
144 |
150 | >
151 | )}
152 |
153 | );
154 | }
155 | );
156 |
157 | export type ReasoningContentProps = ComponentProps<
158 | typeof CollapsibleContent
159 | > & {
160 | children: string;
161 | };
162 |
163 | export const ReasoningContent = memo(
164 | ({ className, children, ...props }: ReasoningContentProps) => (
165 |
173 | {children}
174 |
175 | )
176 | );
177 |
178 | Reasoning.displayName = 'Reasoning';
179 | ReasoningTrigger.displayName = 'ReasoningTrigger';
180 | ReasoningContent.displayName = 'ReasoningContent';
181 |
--------------------------------------------------------------------------------
/src/components/ui/shadcn-io/ai/branch.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Button } from '@/components/ui/button';
4 | import { cn } from '@/lib/utils';
5 | import type { UIMessage } from 'ai';
6 | import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
7 | import type { ComponentProps, HTMLAttributes, ReactElement } from 'react';
8 | import { createContext, useContext, useEffect, useState } from 'react';
9 |
10 | type BranchContextType = {
11 | currentBranch: number;
12 | totalBranches: number;
13 | goToPrevious: () => void;
14 | goToNext: () => void;
15 | branches: ReactElement[];
16 | setBranches: (branches: ReactElement[]) => void;
17 | };
18 |
19 | const BranchContext = createContext(null);
20 |
21 | const useBranch = () => {
22 | const context = useContext(BranchContext);
23 |
24 | if (!context) {
25 | throw new Error('Branch components must be used within Branch');
26 | }
27 |
28 | return context;
29 | };
30 |
31 | export type BranchProps = HTMLAttributes & {
32 | defaultBranch?: number;
33 | onBranchChange?: (branchIndex: number) => void;
34 | };
35 |
36 | export const Branch = ({
37 | defaultBranch = 0,
38 | onBranchChange,
39 | className,
40 | ...props
41 | }: BranchProps) => {
42 | const [currentBranch, setCurrentBranch] = useState(defaultBranch);
43 | const [branches, setBranches] = useState([]);
44 |
45 | const handleBranchChange = (newBranch: number) => {
46 | setCurrentBranch(newBranch);
47 | onBranchChange?.(newBranch);
48 | };
49 |
50 | const goToPrevious = () => {
51 | const newBranch =
52 | currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
53 | handleBranchChange(newBranch);
54 | };
55 |
56 | const goToNext = () => {
57 | const newBranch =
58 | currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
59 | handleBranchChange(newBranch);
60 | };
61 |
62 | const contextValue: BranchContextType = {
63 | currentBranch,
64 | totalBranches: branches.length,
65 | goToPrevious,
66 | goToNext,
67 | branches,
68 | setBranches,
69 | };
70 |
71 | return (
72 |
73 | div]:pb-0', className)}
75 | {...(props as any)}
76 | />
77 |
78 | );
79 | };
80 |
81 | export type BranchMessagesProps = HTMLAttributes
;
82 |
83 | export const BranchMessages = ({ children, ...props }: BranchMessagesProps) => {
84 | const { currentBranch, setBranches, branches } = useBranch();
85 | const childrenArray = Array.isArray(children) ? children : [children];
86 |
87 | // Use useEffect to update branches when they change
88 | useEffect(() => {
89 | if (branches.length !== childrenArray.length) {
90 | setBranches(childrenArray);
91 | }
92 | }, [childrenArray, branches, setBranches]);
93 |
94 | return childrenArray.map((branch, index) => (
95 | div]:pb-0',
98 | index === currentBranch ? 'block' : 'hidden'
99 | )}
100 | key={branch.key}
101 | {...(props as any)}
102 | >
103 | {branch}
104 |
105 | ));
106 | };
107 |
108 | export type BranchSelectorProps = HTMLAttributes & {
109 | from: UIMessage['role'];
110 | };
111 |
112 | export const BranchSelector = ({
113 | className,
114 | from,
115 | ...props
116 | }: BranchSelectorProps) => {
117 | const { totalBranches } = useBranch();
118 |
119 | // Don't render if there's only one branch
120 | if (totalBranches <= 1) {
121 | return null;
122 | }
123 |
124 | return (
125 |
133 | );
134 | };
135 |
136 | export type BranchPreviousProps = ComponentProps;
137 |
138 | export const BranchPrevious = ({
139 | className,
140 | children,
141 | ...props
142 | }: BranchPreviousProps) => {
143 | const { goToPrevious, totalBranches } = useBranch();
144 |
145 | return (
146 |
161 | {children ?? }
162 |
163 | );
164 | };
165 |
166 | export type BranchNextProps = ComponentProps;
167 |
168 | export const BranchNext = ({
169 | className,
170 | children,
171 | ...props
172 | }: BranchNextProps) => {
173 | const { goToNext, totalBranches } = useBranch();
174 |
175 | return (
176 |
191 | {children ?? }
192 |
193 | );
194 | };
195 |
196 | export type BranchPageProps = HTMLAttributes;
197 |
198 | export const BranchPage = ({ className, ...props }: BranchPageProps) => {
199 | const { currentBranch, totalBranches } = useBranch();
200 |
201 | return (
202 |
209 | {currentBranch + 1} of {totalBranches}
210 |
211 | );
212 | };
213 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { cn } from "@/lib/utils"
6 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons"
7 |
8 | const Select = SelectPrimitive.Root
9 |
10 | const SelectGroup = SelectPrimitive.Group
11 |
12 | const SelectValue = SelectPrimitive.Value
13 |
14 | const SelectTrigger = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, children, ...props }, ref) => (
18 | span]:line-clamp-1",
22 | className
23 | )}
24 | {...props}
25 | >
26 | {children}
27 |
28 |
29 |
30 |
31 | ))
32 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
33 |
34 | const SelectScrollUpButton = React.forwardRef<
35 | React.ElementRef,
36 | React.ComponentPropsWithoutRef
37 | >(({ className, ...props }, ref) => (
38 |
46 |
47 |
48 | ))
49 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
50 |
51 | const SelectScrollDownButton = React.forwardRef<
52 | React.ElementRef,
53 | React.ComponentPropsWithoutRef
54 | >(({ className, ...props }, ref) => (
55 |
63 |
64 |
65 | ))
66 | SelectScrollDownButton.displayName =
67 | SelectPrimitive.ScrollDownButton.displayName
68 |
69 | const SelectContent = React.forwardRef<
70 | React.ElementRef,
71 | React.ComponentPropsWithoutRef
72 | >(({ className, children, position = "popper", ...props }, ref) => (
73 |
74 |
85 |
86 |
93 | {children}
94 |
95 |
96 |
97 |
98 | ))
99 | SelectContent.displayName = SelectPrimitive.Content.displayName
100 |
101 | const SelectLabel = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | SelectLabel.displayName = SelectPrimitive.Label.displayName
112 |
113 | const SelectItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, children, ...props }, ref) => (
117 |
125 |
126 |
127 |
128 |
129 |
130 | {children}
131 |
132 | ))
133 | SelectItem.displayName = SelectPrimitive.Item.displayName
134 |
135 | const SelectSeparator = React.forwardRef<
136 | React.ElementRef,
137 | React.ComponentPropsWithoutRef
138 | >(({ className, ...props }, ref) => (
139 |
144 | ))
145 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
146 |
147 | export {
148 | Select,
149 | SelectGroup,
150 | SelectValue,
151 | SelectTrigger,
152 | SelectContent,
153 | SelectLabel,
154 | SelectItem,
155 | SelectSeparator,
156 | SelectScrollUpButton,
157 | SelectScrollDownButton,
158 | }
159 |
--------------------------------------------------------------------------------
/src/components/ui/shadcn-io/ai/prompt-input.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Button } from '@/components/ui/button';
4 | import {
5 | Select,
6 | SelectContent,
7 | SelectItem,
8 | SelectTrigger,
9 | SelectValue,
10 | } from '@/components/ui/select';
11 | import { Textarea } from '@/components/ui/textarea';
12 | import { cn } from '@/lib/utils';
13 | import type { ChatStatus } from 'ai';
14 | import { Loader2Icon, SendIcon, SquareIcon, XIcon } from 'lucide-react';
15 | import type {
16 | ComponentProps,
17 | HTMLAttributes,
18 | KeyboardEventHandler,
19 | } from 'react';
20 | import { Children } from 'react';
21 |
22 | export type PromptInputProps = HTMLAttributes;
23 |
24 | export const PromptInput = ({ className, ...props }: PromptInputProps) => (
25 |
32 | );
33 |
34 | export type PromptInputTextareaProps = ComponentProps & {
35 | minHeight?: number;
36 | maxHeight?: number;
37 | };
38 |
39 | export const PromptInputTextarea = ({
40 | onChange,
41 | className,
42 | placeholder = 'What would you like to know?',
43 | minHeight = 48,
44 | maxHeight = 164,
45 | ...props
46 | }: PromptInputTextareaProps) => {
47 | const handleKeyDown: KeyboardEventHandler = (e) => {
48 | if (e.key === 'Enter') {
49 | if (e.shiftKey) {
50 | // Allow newline
51 | return;
52 | }
53 |
54 | // Submit on Enter (without Shift)
55 | e.preventDefault();
56 | const form = e.currentTarget.form;
57 | if (form) {
58 | form.requestSubmit();
59 | }
60 | }
61 | };
62 |
63 | return (
64 |