From ce236ffc3ca16d12cc31e3aa67afa46297e32dc5 Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Tue, 28 Oct 2025 23:52:11 +0530 Subject: [PATCH 1/2] Add Echo rules for various Next.js applications including Chat, Image Generation, Video Generation, and API Key Management --- .../assistant-ui/.cursor/rules/echo_rules.mdc | 474 ++++++++++++++ .../next-chat/.cursor/rules/echo_rules.mdc | 388 +++++++++++ .../next-image/.cursor/rules/echo_rules.mdc | 431 +++++++++++++ .../.cursor/rules/echo_rules.mdc | 490 ++++++++++++++ templates/next/.cursor/rules/echo_rules.mdc | 336 ++++++++++ .../.cursor/rules/echo_rules.mdc | 567 ++++++++++++++++ .../react-chat/.cursor/rules/echo_rules.mdc | 491 ++++++++++++++ .../react-image/.cursor/rules/echo_rules.mdc | 605 ++++++++++++++++++ templates/react/.cursor/rules/echo_rules.mdc | 378 +++++++++++ 9 files changed, 4160 insertions(+) create mode 100644 templates/assistant-ui/.cursor/rules/echo_rules.mdc create mode 100644 templates/next-chat/.cursor/rules/echo_rules.mdc create mode 100644 templates/next-image/.cursor/rules/echo_rules.mdc create mode 100644 templates/next-video-template/.cursor/rules/echo_rules.mdc create mode 100644 templates/next/.cursor/rules/echo_rules.mdc create mode 100644 templates/nextjs-api-key-template/.cursor/rules/echo_rules.mdc create mode 100644 templates/react-chat/.cursor/rules/echo_rules.mdc create mode 100644 templates/react-image/.cursor/rules/echo_rules.mdc create mode 100644 templates/react/.cursor/rules/echo_rules.mdc diff --git a/templates/assistant-ui/.cursor/rules/echo_rules.mdc b/templates/assistant-ui/.cursor/rules/echo_rules.mdc new file mode 100644 index 000000000..2c326fb71 --- /dev/null +++ b/templates/assistant-ui/.cursor/rules/echo_rules.mdc @@ -0,0 +1,474 @@ +## Description : Guidelines and best practices for building Echo Assistant UI applications with Next.js, including streaming, component patterns, and tool handling +globs: /**/*.ts,/**/*.tsx,**/*.js,**/*.jsx + +# Echo Assistant UI Guidelines + +## SDK Initialization + +### Server-Side Setup + +ALWAYS initialize Echo in `src/echo/index.ts`: + +```typescript +import Echo from '@merit-systems/echo-next-sdk'; + +export const { handlers, isSignedIn, openai, anthropic } = Echo({ + appId: process.env.ECHO_APP_ID!, +}); +``` + +### Client-Side Provider + +ALWAYS wrap your application with `EchoProvider`: + +```typescript +'use client'; + +import { EchoProvider } from '@merit-systems/echo-next-sdk/client'; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +``` + +## Assistant UI Integration + +### Chat API with Streaming + +ALWAYS implement chat routes with streaming for responsive UI: + +```typescript +// app/api/chat/route.ts +import { openai } from '@/echo'; +import { streamText } from 'ai'; + +export const maxDuration = 30; + +export async function POST(req: Request) { + try { + const { messages } = await req.json(); + + // ✅ ALWAYS validate messages + if (!messages || !Array.isArray(messages)) { + return new Response( + JSON.stringify({ + error: 'Bad Request', + message: 'Messages must be an array', + }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + const result = streamText({ + model: openai('gpt-4o'), + messages, + // Enable streaming with sources and reasoning + experimental_toolCallStreaming: true, + }); + + return result.toDataStreamResponse(); + } catch (error) { + console.error('Chat API error:', error); + return new Response( + JSON.stringify({ + error: 'Internal server error', + message: 'Failed to process chat request', + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} +``` + +## Assistant UI Components + +### Thread Component + +Use the assistant-ui Thread component for chat interface: + +```typescript +'use client'; + +import { Thread } from '@assistant-ui/react'; +import { makeMarkdownText } from '@assistant-ui/react-markdown'; + +const MarkdownText = makeMarkdownText(); + +export function ChatInterface() { + return ( + + ); +} +``` + +### Custom Message Components + +ALWAYS create reusable message components: + +```typescript +// components/assistant-ui/message.tsx +'use client'; + +import { FC } from 'react'; +import { MessagePrimitive } from '@assistant-ui/react'; +import { cn } from '@/lib/utils'; + +export const Message: FC = () => { + return ( + + +
+ +
+
+ + +
+ +
+
+
+ ); +}; +``` + +## Tool Handling + +### Define Tools + +ALWAYS define tools with proper validation: + +```typescript +// app/api/chat/route.ts +import { openai } from '@/echo'; +import { streamText, tool } from 'ai'; +import { z } from 'zod'; + +export async function POST(req: Request) { + const { messages } = await req.json(); + + const result = streamText({ + model: openai('gpt-4o'), + messages, + tools: { + // ✅ Define tools with Zod schemas + getWeather: tool({ + description: 'Get the current weather for a location', + parameters: z.object({ + location: z.string().describe('The city and state, e.g. San Francisco, CA'), + }), + execute: async ({ location }) => { + // Implement weather lookup + return { + location, + temperature: 72, + conditions: 'Sunny', + }; + }, + }), + + searchWeb: tool({ + description: 'Search the web for information', + parameters: z.object({ + query: z.string().describe('The search query'), + maxResults: z.number().optional().describe('Maximum number of results'), + }), + execute: async ({ query, maxResults = 5 }) => { + // Implement web search + return { + query, + results: [ + { title: 'Result 1', url: 'https://example.com/1' }, + { title: 'Result 2', url: 'https://example.com/2' }, + ], + }; + }, + }), + }, + }); + + return result.toDataStreamResponse(); +} +``` + +### Tool UI Components + +Create custom components for tool rendering: + +```typescript +// components/assistant-ui/tool-fallback.tsx +'use client'; + +import { ToolFallback } from '@assistant-ui/react'; + +export function CustomToolFallback() { + return ( + ( +
+ {toolName} + {JSON.stringify(args)} +
+ )} + /> + ); +} +``` + +## Markdown Rendering + +### Markdown Text Component + +ALWAYS use safe markdown rendering: + +```typescript +// components/assistant-ui/markdown-text.tsx +'use client'; + +import { makeMarkdownText } from '@assistant-ui/react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeHighlight from 'rehype-highlight'; + +export const MarkdownText = makeMarkdownText({ + remarkPlugins: [remarkGfm], + rehypePlugins: [rehypeHighlight], + components: { + // Custom component overrides + code: ({ className, children, ...props }) => { + const match = /language-(\w+)/.exec(className || ''); + return match ? ( +
+          {children}
+        
+ ) : ( + + {children} + + ); + }, + }, +}); +``` + +## Environment Variables + +ALWAYS use environment variables: + +```bash +# .env.local +ECHO_APP_ID=your_echo_app_id +NEXT_PUBLIC_ECHO_APP_ID=your_echo_app_id +``` + +NEVER hardcode credentials: + +```typescript +// ✅ CORRECT +const appId = process.env.ECHO_APP_ID!; + +// ❌ INCORRECT +const appId = 'echo_app_123abc'; +``` + +## TypeScript Types + +### Message and Tool Types + +ALWAYS define strict types: + +```typescript +// src/lib/types.ts +export interface ChatMessage { + id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + createdAt: number; + toolCalls?: ToolCall[]; +} + +export interface ToolCall { + id: string; + name: string; + arguments: Record; + result?: any; +} + +export interface Tool { + name: string; + description: string; + parameters: Record; + execute: (args: any) => Promise; +} +``` + +## Feature Flags + +### Assistant UI Feature Flags + +ALWAYS centralize feature flags: + +```typescript +// src/lib/flags.ts +export enum AssistantFeatureFlags { + ENABLE_TOOL_CALLING = 'enable_tool_calling', + ENABLE_REASONING = 'enable_reasoning', + ENABLE_SOURCES = 'enable_sources', + ENABLE_MARKDOWN = 'enable_markdown', +} + +export function validateAssistantFeatureFlag(flag: string): boolean { + return Object.values(AssistantFeatureFlags).includes(flag as AssistantFeatureFlags); +} +``` + +## Component Organization + +### Recommended Structure + +Structure assistant components for reusability: + +``` +src/ +├── app/ +│ ├── api/ +│ │ ├── chat/ +│ │ │ └── route.ts # Chat endpoint with tools +│ │ └── echo/ +│ │ └── [...echo]/route.ts +│ ├── layout.tsx +│ └── page.tsx +├── components/ +│ └── assistant-ui/ +│ ├── markdown-text.tsx # Markdown renderer +│ ├── thread.tsx # Thread wrapper +│ ├── tool-fallback.tsx # Tool UI fallback +│ └── tooltip-icon-button.tsx +├── echo/ +│ └── index.ts # Server Echo init +├── lib/ +│ ├── flags.ts # Feature flags +│ ├── types.ts # Shared types +│ └── utils.ts # Utilities +└── providers.tsx # Client providers +``` + +## Streaming Best Practices + +### Proper Streaming Setup + +ALWAYS use streaming for responsive UX: + +```typescript +// ✅ CORRECT - Full streaming with tool calls +const result = streamText({ + model: openai('gpt-4o'), + messages, + tools, + experimental_toolCallStreaming: true, +}); + +return result.toDataStreamResponse(); + +// ❌ INCORRECT - No streaming +const { text } = await generateText({ + model: openai('gpt-4o'), + messages, +}); + +return Response.json({ text }); +``` + +## Error Handling + +ALWAYS handle errors with clear messages: + +```typescript +try { + const result = streamText({ + model: openai('gpt-4o'), + messages, + tools, + }); + + return result.toDataStreamResponse(); +} catch (error) { + console.error('Chat error:', error); + + // Provide specific error messages + if (error instanceof AuthenticationError) { + return new Response( + JSON.stringify({ error: 'Authentication failed' }), + { status: 401 } + ); + } + + if (error instanceof RateLimitError) { + return new Response( + JSON.stringify({ error: 'Rate limit exceeded' }), + { status: 429 } + ); + } + + return new Response( + JSON.stringify({ error: 'Internal server error' }), + { status: 500 } + ); +} +``` + +## Testing + +NEVER call external services in tests. ALWAYS mock: + +```typescript +import { vi } from 'vitest'; + +vi.mock('@/echo', () => ({ + openai: vi.fn(() => mockOpenAI), +})); + +describe('Chat API with Assistant UI', () => { + it('should stream responses', async () => { + const response = await POST(mockRequest); + expect(response.status).toBe(200); + }); + + it('should handle tool calls', async () => { + // Test tool execution + }); +}); +``` + +## Best Practices + +1. **Streaming First**: Always use streaming for responsive assistant UX +2. **Tool Definition**: Define tools with Zod schemas for type safety +3. **Component Reuse**: Create reusable assistant-ui components +4. **Markdown Safety**: Use safe markdown rendering with plugins +5. **Error Handling**: Provide specific, actionable error messages +6. **Type Safety**: Export and use strict TypeScript types +7. **Feature Flags**: Centralize flags in one file, validate before use +8. **Testing**: Mock external services and test streaming behavior diff --git a/templates/next-chat/.cursor/rules/echo_rules.mdc b/templates/next-chat/.cursor/rules/echo_rules.mdc new file mode 100644 index 000000000..baf220e30 --- /dev/null +++ b/templates/next-chat/.cursor/rules/echo_rules.mdc @@ -0,0 +1,388 @@ +## Description: Guidelines and best practices for building Echo Next.js Chat applications, including streaming, server/client boundaries, and chat UI patterns +globs: /**/*.ts,/**/*.tsx,**/*.js,**/*.jsx + +# Echo Next.js Chat Guidelines + +## SDK Initialization + +### Server-Side Setup + +ALWAYS initialize Echo in `src/echo/index.ts` for server-side AI calls: + +```typescript +import Echo from '@merit-systems/echo-next-sdk'; + +export const { handlers, isSignedIn, openai, anthropic } = Echo({ + appId: process.env.ECHO_APP_ID!, +}); +``` + +### Client-Side Provider + +ALWAYS wrap your application with `EchoProvider`: + +```typescript +'use client'; + +import { EchoProvider } from '@merit-systems/echo-next-sdk/client'; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +``` + +## Chat API Implementation + +### Streaming Chat Route + +ALWAYS implement chat endpoints with proper validation and streaming: + +```typescript +// app/api/chat/route.ts +import { convertToModelMessages, streamText, type UIMessage } from 'ai'; +import { openai } from '@/echo'; + +// Allow streaming responses up to 30 seconds +export const maxDuration = 30; + +export async function POST(req: Request) { + try { + const { + model, + messages, + }: { + messages: UIMessage[]; + model: string; + } = await req.json(); + + // ✅ ALWAYS validate required parameters + if (!model) { + return new Response( + JSON.stringify({ + error: 'Bad Request', + message: 'Model parameter is required', + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + if (!messages || !Array.isArray(messages)) { + return new Response( + JSON.stringify({ + error: 'Bad Request', + message: 'Messages parameter is required and must be an array', + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + const result = streamText({ + model: openai(model), + messages: convertToModelMessages(messages), + }); + + return result.toUIMessageStreamResponse({ + sendSources: true, + sendReasoning: true, + }); + } catch (error) { + console.error('Chat API error:', error); + return new Response( + JSON.stringify({ + error: 'Internal server error', + message: 'Failed to process chat request', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ); + } +} +``` + +## Client-Side Chat UI + +### Using the useChat Hook + +Use the `useChat` hook from Echo React SDK for chat state management: + +```typescript +'use client'; + +import { useChat, useEcho } from '@merit-systems/echo-react-sdk'; +import { useState } from 'react'; + +export function ChatInterface() { + const [input, setInput] = useState(''); + const { messages, sendMessage, status } = useChat(); + const { user } = useEcho(); + + const isSignedIn = user !== null; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (input.trim() && isSignedIn) { + sendMessage({ + role: 'user', + content: input, + }); + setInput(''); + } + }; + + return ( +
+
+ {messages.map((message, index) => ( +
+
{message.content}
+
+ ))} + {status === 'pending' &&
Thinking...
} +
+ +
+ setInput(e.target.value)} + disabled={!isSignedIn || status === 'pending'} + placeholder={isSignedIn ? 'Type a message...' : 'Sign in to chat'} + /> + +
+
+ ); +} +``` + +## Streaming Responses + +### Proper Streaming Setup + +ALWAYS use streaming for responsive chat experiences: + +```typescript +// ✅ CORRECT - Using streaming +const result = streamText({ + model: openai('gpt-4o'), + messages: convertToModelMessages(messages), +}); + +return result.toUIMessageStreamResponse({ + sendSources: true, + sendReasoning: true, +}); + +// ❌ INCORRECT - Using non-streaming (blocks until complete) +const result = await generateText({ + model: openai('gpt-4o'), + messages: convertToModelMessages(messages), +}); + +return Response.json({ text: result.text }); +``` + +## Environment Variables + +ALWAYS store credentials in `.env.local`: + +```bash +# Server-side only +ECHO_APP_ID=your_echo_app_id + +# Client-side (public) +NEXT_PUBLIC_ECHO_APP_ID=your_echo_app_id +``` + +NEVER hardcode API keys: + +```typescript +// ✅ CORRECT +const appId = process.env.ECHO_APP_ID!; + +// ❌ INCORRECT +const appId = 'echo_app_123abc'; +``` + +## Feature Flags and Custom Properties + +### Centralized Feature Flags + +ALWAYS define feature flags in a single constants file: + +```typescript +// src/lib/flags.ts +export enum ChatFeatureFlags { + ENABLE_VOICE_INPUT = 'enable_voice_input', + ENABLE_FILE_UPLOAD = 'enable_file_upload', + ENABLE_REASONING = 'enable_reasoning', + ENABLE_SOURCES = 'enable_sources', +} + +export function validateChatFeatureFlag(flag: string): boolean { + return Object.values(ChatFeatureFlags).includes(flag as ChatFeatureFlags); +} +``` + +### Custom Properties + +If a custom property is used multiple times, define it once: + +```typescript +// src/lib/properties.ts +export const ChatProperties = { + MESSAGE_COUNT: 'message_count', + CONVERSATION_ID: 'conversation_id', + MODEL_PREFERENCE: 'model_preference', +} as const; + +export type ChatProperty = typeof ChatProperties[keyof typeof ChatProperties]; +``` + +## TypeScript Types + +### Message Types + +ALWAYS export shared types for chat messages: + +```typescript +// src/lib/types.ts +export interface ChatMessage { + id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + createdAt: number; + metadata?: { + model?: string; + tokens?: number; + sources?: Source[]; + }; +} + +export interface Source { + id: string; + title: string; + url: string; + snippet?: string; +} + +export interface ChatState { + messages: ChatMessage[]; + isStreaming: boolean; + error: string | null; +} +``` + +## Chat UI Components + +### Component Organization + +Structure chat components for reusability: + +``` +src/ +├── app/ +│ ├── api/ +│ │ └── chat/ +│ │ └── route.ts # Chat API endpoint +│ └── page.tsx # Main chat page +├── components/ +│ ├── ai-elements/ +│ │ ├── message.tsx # Message display +│ │ ├── conversation.tsx # Conversation container +│ │ ├── prompt-input.tsx # User input +│ │ └── loader.tsx # Loading states +│ └── echo-account.tsx # Account management +├── echo/ +│ └── index.ts # Server-side Echo init +├── lib/ +│ ├── flags.ts # Feature flags +│ ├── properties.ts # Custom properties +│ └── types.ts # Shared types +└── providers.tsx # Client providers +``` + +## Error Handling + +ALWAYS handle errors with clear messages and appropriate status codes: + +```typescript +// ✅ CORRECT +try { + const result = await streamText({ + model: openai(model), + messages: convertToModelMessages(messages), + }); + return result.toUIMessageStreamResponse(); +} catch (error) { + console.error('Chat error:', error); + + if (error instanceof AuthenticationError) { + return new Response( + JSON.stringify({ error: 'Authentication failed' }), + { status: 401 } + ); + } + + return new Response( + JSON.stringify({ error: 'Internal server error' }), + { status: 500 } + ); +} + +// ❌ INCORRECT +try { + const result = await streamText({ + model: openai(model), + messages: convertToModelMessages(messages), + }); + return result.toUIMessageStreamResponse(); +} catch (error) { + // Silent failure or generic error + return new Response('Error', { status: 500 }); +} +``` + +## Testing + +NEVER call external services in tests. ALWAYS mock: + +```typescript +import { vi } from 'vitest'; + +vi.mock('@/echo', () => ({ + openai: vi.fn(() => mockOpenAI), +})); + +describe('Chat API', () => { + it('should stream chat responses', async () => { + const response = await POST(mockRequest); + expect(response.status).toBe(200); + }); +}); +``` + +## Best Practices + +1. **Streaming First**: Always use streaming for responsive UX +2. **Validation**: Validate all inputs in API routes +3. **Error Handling**: Provide clear, actionable error messages +4. **Type Safety**: Export and use strict TypeScript types +5. **Feature Flags**: Centralize in one file, validate before use +6. **Component Structure**: Keep chat components modular and reusable +7. **Security**: Never expose server secrets to client components +8. **Performance**: Use proper loading states and optimistic updates diff --git a/templates/next-image/.cursor/rules/echo_rules.mdc b/templates/next-image/.cursor/rules/echo_rules.mdc new file mode 100644 index 000000000..17879ea56 --- /dev/null +++ b/templates/next-image/.cursor/rules/echo_rules.mdc @@ -0,0 +1,431 @@ +## Description: Guidelines and best practices for building Echo Next.js Image Generation applications, including API routes, file handling, billing, and security +globs: /**/*.ts,/**/*.tsx,**/*.js,**/*.jsx + +# Echo Next.js Image Generation Guidelines + +## SDK Initialization + +### Server-Side Setup + +ALWAYS initialize Echo in `src/echo/index.ts`: + +```typescript +import Echo from '@merit-systems/echo-next-sdk'; + +export const { handlers, isSignedIn, openai, anthropic } = Echo({ + appId: process.env.ECHO_APP_ID!, +}); +``` + +## Image Generation API + +### Image Generation Route + +ALWAYS implement image generation with proper validation and error handling: + +```typescript +// app/api/generate-image/route.ts +import { openai } from '@/echo'; +import { generateImage } from 'ai'; + +export const maxDuration = 60; // Image generation may take longer + +export async function POST(req: Request) { + try { + const { prompt, model, size } = await req.json(); + + // ✅ ALWAYS validate required parameters + if (!prompt || typeof prompt !== 'string') { + return new Response( + JSON.stringify({ + error: 'Bad Request', + message: 'Prompt is required and must be a string', + }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + if (!model) { + return new Response( + JSON.stringify({ + error: 'Bad Request', + message: 'Model parameter is required', + }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Validate size parameter + const validSizes = ['1024x1024', '1792x1024', '1024x1792']; + if (size && !validSizes.includes(size)) { + return new Response( + JSON.stringify({ + error: 'Bad Request', + message: `Size must be one of: ${validSizes.join(', ')}`, + }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + const { image } = await generateImage({ + model: openai.image(model), + prompt, + size: size || '1024x1024', + }); + + return Response.json({ + success: true, + image: { + url: image.url, + prompt, + model, + size: size || '1024x1024', + }, + }); + } catch (error) { + console.error('Image generation error:', error); + + if (error instanceof Error && error.message.includes('content_policy')) { + return new Response( + JSON.stringify({ + error: 'Content Policy Violation', + message: 'The prompt violates the content policy', + }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + return new Response( + JSON.stringify({ + error: 'Internal server error', + message: 'Failed to generate image', + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} +``` + +## Client-Side Image Generation + +### Using Image Generation Hook + +Implement client-side image generation UI with proper loading states: + +```typescript +'use client'; + +import { useState } from 'react'; +import { useEcho } from '@merit-systems/echo-react-sdk'; + +interface GeneratedImage { + url: string; + prompt: string; + model: string; + size: string; +} + +export function ImageGenerator() { + const [prompt, setPrompt] = useState(''); + const [image, setImage] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { user } = useEcho(); + + const handleGenerate = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!user) { + setError('Please sign in to generate images'); + return; + } + + if (!prompt.trim()) { + setError('Please enter a prompt'); + return; + } + + setLoading(true); + setError(null); + + try { + const response = await fetch('/api/generate-image', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + prompt, + model: 'dall-e-3', + size: '1024x1024', + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Failed to generate image'); + } + + const data = await response.json(); + setImage(data.image); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+