This is a React Router 7 application that combines generative AI with industry-agnostic project management, featuring a chat-based interface with dynamically generated UI components. The application adapts to various industries through configurable terminology and workflows.
- React Router 7: Use React Router 7 framework mode with file-based routing, server-side rendering, and type-safe route definitions
- Always prefer React Router 7 patterns over Next.js or other React frameworks
- Use
loader
functions for data fetching andaction
functions for mutations - Implement route-based code splitting and progressive enhancement
- Tailwind CSS 4: Use Tailwind CSS utility classes for ALL styling - never use inline styles or custom CSS
- daisyUI 5: Use daisyUI component classes following the semantic naming convention (e.g.,
btn
,card
,chat-bubble
) - CVA (Class Variance Authority): Use the configured CVA setup in
~/cva.config.ts
withtwMerge
integration - NEVER use inline styles: Always convert to Tailwind utility classes or daisyUI components
- Always prefer daisyUI color names (
primary
,secondary
,base-100
) over Tailwind color names for theme consistency - Use responsive design patterns with mobile-first approach (
sm:
,md:
,lg:
,xl:
) - CSS customization: Only use
@apply
directive in CSS files when absolutely necessary for complex components
- Install:
@plugin "daisyui";
in CSS file, not tailwind.config.js (deprecated in v4) - Component structure: Add component class (
btn
), part classes if available, and modifier classes - Customization: Use Tailwind utilities alongside daisyUI classes (e.g.,
btn px-10
) - Force override: Use
!
suffix for CSS specificity issues (e.g.,btn bg-red-500!
) as last resort - Color system: Use semantic color names (
primary
,error
,base-100
) that adapt to themes - No custom CSS: Prefer daisyUI + Tailwind utilities over writing custom styles
- Better Auth: Use Better Auth for all authentication needs
- Server-side auth instance is in
~/utils/auth.server.ts
with Prisma adapter - Client-side auth utilities are in
~/utils/auth.ts
with React hooks - Always check session status using
auth.api.getSession()
on server anduseSession()
on client - Implement role-based access control using the Role enum (ADMIN, USER, PROVIDER, CLIENT, MANAGER, etc.)
- Prisma: Use Prisma ORM with PostgreSQL database
- Generated Prisma client is located at
~/generated/prisma/client.ts
- Always use the configured Prisma instance from
~/lib/prisma.ts
- Follow the established schema patterns for User, WorkItem, Thread, Message, and Task models
- Use proper error handling and transaction patterns
- Vercel AI SDK: Use AI SDK for chat functionality with OpenAI integration
- OpenAI API: Integrate OpenAI models for generative responses and tool calling
- Use streaming responses with the
useChat
hook for real-time chat updates - Implement tool invocations for project management actions
- Follow the established message conversion patterns using
convertToUIMessage
- Components go in
~/components/
with UI-specific components in~/components/ui/
- Server-side business logic goes in
~/models/
- Utilities go in
~/utils/
- Routes follow React Router 7 file-based routing in
~/routes/
- API routes are in
~/routes/api/
for backend functionality
app/routes/
├── authenticated.tsx # Layout route with auth checks
├── home.tsx # Public pages
├── chat.tsx # Feature layout routes
├── thread.tsx # Dynamic routes
└── api/ # API-only routes
├── auth.ts # Better Auth handler
├── chat.ts # AI chat endpoint
└── tools/ # AI tool functions
app/components/
├── Button.tsx # Base UI components
├── Card.tsx
├── TextField.tsx
└── ui/ # Feature-specific components
├── CreateWorkItemForm.tsx
├── MessageRenderer.tsx
└── FrequentMessageManager.tsx
- Use functional components with TypeScript
- Implement proper prop interfaces with descriptive names
- Use the CVA pattern for component variants (see existing components like
Button
,ChatBubble
) - Follow the composable component pattern for complex UI elements
-
Use
~
alias for imports from the app directory -
Import types with
import type
syntax -
Group imports in this order:
// React/Framework imports first import { useState } from 'react'; import { useFetcher, useLoaderData } from 'react-router'; // Third-party libraries import { PlusIcon } from 'lucide-react'; // Local imports (using ~ alias) import { Button } from '~/components/Button'; import { getUserIdFromRequest } from '~/utils/request.server'; import type { Route } from './+types/route-name';
- Use proper error boundaries and error handling patterns
- Implement loading states and optimistic UI updates
- Handle database errors gracefully with user-friendly messages
- Always use
invariant()
for required data in loaders/actions - Throw
Response
objects for HTTP errors in loaders/actions - Use
data()
utility with error status for API routes - Export
ErrorBoundary
components from route modules
- Use the established
MessageRenderer
andBaseChatCard
components for displaying chat messages - Implement tool calling capabilities for project management actions
- Support rich message types including forms, cards, and interactive elements
- Maintain conversation context across sessions
- Generate appropriate UI components based on user context and project data
- Use role-based component rendering (different UI for ADMIN vs USER)
- Implement contextual action buttons and forms within chat interface
- Support embedded forms for project creation and editing
This application is designed for flexible project management with the following domain-agnostic considerations:
- Projects/Work Items have configurable status tracking (ACTIVE, PENDING, IN_PROGRESS, ON_HOLD, COMPLETED, CANCELLED)
- Users have different roles (ADMIN, PROVIDER, CLIENT, MANAGER, USER) with varying permissions
- Task management includes work items and issue tracking with customizable terminology
- Email notifications and team communication are core features
- Timeline and progress tracking are essential for business workflows
- Business configuration system allows adaptation to different industries (Construction, Consulting, Healthcare, Legal, etc.)
- Configurable terminology maps generic concepts to industry-specific language (Projects→Cases, Tasks→Issues, etc.)
- Use React Router 7's built-in optimizations for data loading and caching
- Implement proper loading states for chat streams and data fetching
- Use Prisma's efficient querying patterns with proper includes and selections
- Optimize chat message rendering with virtualization for long conversations
- Implement proper error boundaries and fallback UI components
- Always use TypeScript with strict type checking
- Follow the established patterns for server/client code separation
- Use configurable validation functions from model files (
validateTaskData
,validateWorkItemData
,validateUserData
) - Use Zod for runtime validation of forms and API inputs when additional schema validation is needed
- Always validate data server-side in actions/loaders before database operations
- Implement proper SEO and accessibility patterns
- Test authentication flows and role-based access thoroughly
- Ensure responsive design works across all device sizes
- Use React Router 7 API routes in
~/routes/api/
for backend functionality - Implement proper CORS and security headers
- Use Better Auth's built-in CSRF protection
- Follow RESTful patterns for data APIs
- Implement proper rate limiting and validation
This section defines the core patterns used throughout the TWS Generative UI application to ensure consistency and maintainability.
-
React Router 7 Loaders (
loader
functions):- Initial page loads, navigation, URL changes
- Server-side data fetching with automatic revalidation
- Access via
useLoaderData<typeof loader>()
orloaderData
prop
-
React Router 7 Actions (
action
functions):- Form submissions, data mutations that require navigation
- Server-side mutations with automatic loader revalidation
- Access via
useActionData()
oractionData
prop
-
useFetcher for Non-Navigational Operations:
- UI updates without navigation (toggles, inline edits, chat interactions)
- Background data loading (dropdowns, search suggestions)
- Use
fetcher.state
for loading states,fetcher.data
for results
-
useChat for AI Interactions:
- Real-time streaming chat with OpenAI
- Tool calling and generative UI responses
- Message persistence and conversation management
export async function loader({ request }: Route.LoaderArgs) {
const userId = await getUserIdFromRequest({ request });
invariant(userId, 'User must be logged in');
return { data: await fetchData({ userId }) };
}
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const intent = String(formData.get('intent'));
if (intent === 'create') {
// Handle creation logic
return data({ success: true, item });
}
return data({ error: 'Invalid intent' }, { status: 400 });
}
<Card variant="bordered" size="full">
<CardBody>
<h2 className="card-title">Create New {config.terminology.workItem}</h2>
<form onSubmit={handleSubmit}>
{/* Form fields using TextField, Select, etc. */}
</form>
</CardBody>
</Card>
All UI components use Class Variance Authority (CVA) for consistent styling and type safety:
import { cva, type VariantProps } from 'cva';
const buttonVariants = cva({
base: ['btn', 'transition-colors'],
variants: {
variant: {
primary: 'btn-primary',
secondary: 'btn-secondary',
error: 'btn-error',
ghost: 'btn-ghost',
},
size: {
sm: 'btn-sm',
md: 'btn-md',
lg: 'btn-lg',
},
disabled: {
false: null,
true: ['opacity-50', 'cursor-not-allowed'],
},
},
compoundVariants: [
{
variant: 'primary',
disabled: false,
class: 'hover:btn-primary-focus',
},
],
defaultVariants: {
variant: 'primary',
size: 'md',
disabled: false,
},
});
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
children: React.ReactNode;
}
export const Button: React.FC<ButtonProps> = ({
className,
variant,
size,
disabled,
children,
...props
}) => (
<button
className={buttonVariants({ variant, size, disabled, className })}
disabled={disabled || undefined}
{...props}
>
{children}
</button>
);
interface FormProps {
onSubmit: (data: FormData) => void;
onCancel: () => void;
config: IndustryConfiguration;
isLoading?: boolean;
initialData?: any;
validationErrors?: string[];
}
export function CustomForm({
onSubmit,
onCancel,
config,
isLoading,
initialData,
validationErrors,
}: FormProps) {
const [formData, setFormData] = useState<FormData>(initialData || {});
const [errors, setErrors] = useState<string[]>(validationErrors || []);
// Use controlled components with validation
// Implement real-time validation using server validation functions
// Display errors using consistent error handling patterns
return (
<Card variant="bordered" size="full">
<CardBody>
<CardTitle>Create New {config.terminology.workItem}</CardTitle>
<form onSubmit={handleSubmit} className="space-y-4">
<TextField
label={`${config.terminology.workItem} Title`}
value={formData.title}
onChange={(e) =>
setFormData((prev) => ({ ...prev, title: e.target.value }))
}
error={errors.find((e) => e.includes('title'))}
required
/>
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={onCancel}>
Cancel
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? <Spinner size="sm" /> : 'Create'}
</Button>
</div>
</form>
</CardBody>
</Card>
);
}
// Server-side data loading with type safety
export async function loader({ request, params }: Route.LoaderArgs) {
const session = await auth.api.getSession(request);
invariant(session, 'User must be authenticated');
// Parallel data loading for better performance
const [workItems, tasks, config] = await Promise.all([
getWorkItemsForTenant(session.user.tenantId),
getTasksForTenant(session.user.tenantId),
getBusinessConfiguration(session.user.tenantId),
]);
return {
workItems,
tasks,
config,
user: session.user,
};
}
// Client-side data loading for dynamic content
export async function clientLoader({
params,
serverLoader,
}: Route.ClientLoaderArgs) {
const serverData = await serverLoader();
// Add client-specific data (e.g., cached user preferences)
const userPreferences = getUserPreferencesFromLocalStorage();
return {
...serverData,
userPreferences,
};
}
// Forms work with and without JavaScript
export function CreateWorkItemForm() {
const fetcher = useFetcher();
const isSubmitting = fetcher.state === 'submitting';
return (
<Card>
<CardBody>
<fetcher.Form
method="post"
action="/api/work-items"
className="space-y-4"
>
<input type="hidden" name="intent" value="create" />
<TextField
name="title"
label="Title"
required
disabled={isSubmitting}
/>
<TextField
name="description"
label="Description"
multiline
rows={4}
disabled={isSubmitting}
/>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? <Spinner size="sm" /> : 'Create Work Item'}
</Button>
</fetcher.Form>
</CardBody>
</Card>
);
}
// Route-level error boundary
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div className="flex min-h-screen items-center justify-center">
<Card variant="error">
<CardBody>
<CardTitle>Error {error.status}</CardTitle>
<p>{error.data || error.statusText}</p>
<Button as={Link} to="/" variant="primary">
Go Home
</Button>
</CardBody>
</Card>
</div>
);
}
return (
<div className="flex min-h-screen items-center justify-center">
<Card variant="error">
<CardBody>
<CardTitle>Something went wrong</CardTitle>
<p>{error?.message || 'An unexpected error occurred'}</p>
<Button onClick={() => window.location.reload()}>Try Again</Button>
</CardBody>
</Card>
</div>
);
}
// Global loading indicator
export function LoadingIndicator() {
const navigation = useNavigation();
const isLoading = navigation.state === 'loading';
if (!isLoading) return null;
return (
<div className="fixed left-0 right-0 top-0 z-50">
<div className="bg-base-200 h-1">
<div className="bg-primary h-full animate-pulse" />
</div>
</div>
);
}
// Defer non-critical data for faster page loads
export async function loader({ params }: Route.LoaderArgs) {
// Critical data - wait for this
const workItem = await getWorkItem(params.id);
// Non-critical data - defer this
const analytics = getWorkItemAnalytics(params.id); // Returns Promise
return {
workItem,
analytics, // Deferred promise
};
}
export default function WorkItemDetails({ loaderData }: Route.ComponentProps) {
return (
<div className="space-y-6">
{/* Critical content renders immediately */}
<Card>
<CardBody>
<CardTitle>{loaderData.workItem.title}</CardTitle>
<p>{loaderData.workItem.description}</p>
</CardBody>
</Card>
{/* Non-critical content streams in */}
<React.Suspense fallback={<AnalyticsSkeleton />}>
<Await resolve={loaderData.analytics}>
{(analytics) => <AnalyticsChart data={analytics} />}
</Await>
</React.Suspense>
</div>
);
}
export function TaskStatusToggle({ task }: { task: Task }) {
const fetcher = useFetcher();
// Optimistic state based on pending submission
const isCompleted = fetcher.formData
? fetcher.formData.get('status') === 'COMPLETED'
: task.status === 'COMPLETED';
return (
<fetcher.Form method="post" action={`/api/tasks/${task.id}/toggle-status`}>
<input
type="hidden"
name="status"
value={isCompleted ? 'IN_PROGRESS' : 'COMPLETED'}
/>
<Button
type="submit"
variant={isCompleted ? 'success' : 'ghost'}
size="sm"
disabled={fetcher.state === 'submitting'}
>
{isCompleted ? (
<>
<CheckIcon className="h-4 w-4" />
Completed
</>
) : (
<>
<CircleIcon className="h-4 w-4" />
Mark Complete
</>
)}
</Button>
</fetcher.Form>
);
}