| name | writing-typescript-code |
| description | Provides TypeScript and React coding standards for this repository. Use when writing or modifying TypeScript code, creating React components, implementing MSAL authentication, or working with the frontend. |
TypeScript Coding Standards
Goal: Write type-safe React components with proper MSAL integration
Hot Module Replacement (HMR) Workflow
The frontend runs with Vite HMR. When you edit TypeScript/React code:
- Save the file - Vite instantly updates the browser (no refresh needed)
- Check the terminal - Look for HMR updates in the "Frontend: React Vite" terminal
- State is preserved - React state persists through most edits
VS Code Tasks (use Run Task command or check terminal panel):
Frontend: React Vite - Runs npm run dev with HMR enabled
- Logs are visible directly in VS Code terminal
No restart needed - Just edit, save, and see changes instantly in the browser.
Testing changes: Use Playwright browser tools to:
- Navigate to http://localhost:5173
- Check browser console logs for state transitions and errors
- Inspect network requests for API validation
TypeScript Config
Enable strict mode + explicit types (avoid any):
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}
React Components
Use functional components + hooks + typed props:
interface MessageProps {
message: string;
sender: 'user' | 'agent';
}
function Message({ message, sender }: MessageProps) {
return <div className={`msg-${sender}`}>{message}</div>;
}
MSAL Pattern
Always: Try silent first, fallback to popup:
try {
const { accessToken } = await instance.acquireTokenSilent({
...tokenRequest,
account: accounts[0]
});
return accessToken;
} catch {
const { accessToken } = await instance.acquireTokenPopup(tokenRequest);
return accessToken;
}
Environment Variables
CRITICAL: Access at module level only (build-time replacement):
const clientId = import.meta.env.VITE_ENTRA_SPA_CLIENT_ID;
function getClientId() {
return import.meta.env.VITE_ENTRA_SPA_CLIENT_ID;
}
Available variables:
VITE_ENTRA_SPA_CLIENT_ID - Entra app client ID
VITE_ENTRA_TENANT_ID - Azure tenant ID
State Management
Use useState (local) or Context API (shared):
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
Memoization Patterns
Use useMemo and useCallback for expensive computations and stable references:
const isAuthenticated = useMemo(
() => accounts.length > 0,
[accounts.length]
);
const getAccessToken = useCallback(async () => {
}, [instance, accounts]);
return useMemo(
() => ({ getAccessToken, isAuthenticated, user }),
[getAccessToken, isAuthenticated, user]
);
API Calls
Include Authorization header + use async/await:
const response = await fetch('/api/endpoint', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
npm Dependencies
frontend/.npmrc sets legacy-peer-deps=true automatically — no flag needed when running from frontend/.
Gotcha: legacy-peer-deps skips automatic peer dependency installation. If a package requires peer deps, add them explicitly to package.json.
Example: @lexical/yjs requires yjs as a peer dependency. Since peer deps aren't auto-installed, yjs must be in package.json directly.
Before committing package changes, verify with:
npm ci
Common Mistakes
- ❌ Accessing
import.meta.env.* in functions
- ❌ Calling hooks conditionally or in loops
- ❌ Using
any type
- ❌ Storing tokens in component state
- ❌ Running
npm install outside frontend/ (misses .npmrc config)
- ❌ Missing memoization in custom hooks (causes infinite re-renders)
- ❌ Returning new objects from hooks without
useMemo
Project-Specific: Architecture
| Concern | Implementation |
|---|
| State Management | Centralized Context + useReducer (AppContext) with discriminated action union |
| Authentication | MSAL redirect flow; silent token refresh; useAuth hook |
| Chat Streaming | SSE in ChatService with abort controllers for cancellation |
| Accessibility | Live region (aria-live), aria labels, focus management |
| Logging | Dev-only diff-based logger |
Project-Specific: Key Components
| Component | Purpose |
|---|
AgentChat.tsx | Container wiring chat state to controlled ChatInterface |
ChatInterface.tsx | Stateless controlled UI; renders messages, input, errors, BuiltWithBadge |
chat/AssistantMessage.tsx | Memoized assistant message with streaming + citation footnotes |
chat/UserMessage.tsx | Memoized user message with image thumbnail previews |
chat/ChatInput.tsx | File uploads, character counter, cancel streaming button |
chat/CitationMarker.tsx | Inline superscript citation badge with tooltip + click handler |
core/Markdown.tsx | Renders markdown with inline citation markers via ContentWithCitations |
core/BuiltWithBadge.tsx | "Built with Microsoft Foundry" link badge (centered under input) |
Project-Specific: Citation System
Parser: frontend/src/utils/citationParser.ts
Handles Azure AI Agent citation formats:
- Assistants/Responses API:
【4:0†source】, 【13†myfile.pdf】
- Azure OpenAI On Your Data:
[doc1], [doc2]
Flow:
parseContentWithCitations() replaces placeholders with [N] markers
Markdown.tsx renders CitationMarker components for each [N]
- Clicking inline marker scrolls to footnote (with highlight animation) or opens URL
AssistantMessage.tsx renders footnote list with icons by type (URI/file/document)
Key Types (frontend/src/types/chat.ts):
IAnnotation - Citation metadata (type, label, url, fileId, quote, textToReplace)
IndexedCitation - Parsed citation with display index
Project-Specific: File Upload Validation
Limits: 5MB per file, max 5 files total
See: frontend/src/utils/fileAttachments.ts for validateImageFile() and validateFileCount()
Project-Specific: ChatService
File: frontend/src/services/chatService.ts
Key patterns:
- Class-based service with
Dispatch<AppAction> for state updates
AbortController for stream cancellation (cancelStream())
retryWithBackoff() for resilient API calls (3 retries, 1s initial delay)
- SSE parsing via
parseSseLine() and splitSseBuffer() utilities
- Duplicate chunk suppression to prevent UI flicker
Methods:
| Method | Purpose |
|---|
sendMessage() | Orchestrates auth, file conversion, streaming |
cancelStream() | Aborts active stream, dispatches CHAT_CANCEL_STREAM |
clearChat() | Resets conversation state |
clearError() | Clears error without affecting chat |
Project-Specific: Adding Features
- Extend state: Add discriminated action to
AppAction union in frontend/src/types/appState.ts
- Handle in reducer: Update
frontend/src/reducers/appReducer.ts (keep pure, no side effects)
- Create service method: Add to
ChatService if network interaction needed
- Wire container: Update
AgentChat.tsx to dispatch actions
- Update UI: Pass callbacks to controlled component
Project-Specific: Accessibility Checklist
- ✅ Live region announces latest assistant message
- ✅
aria-busy attribute on messages container during streaming
- ✅ Buttons have
aria-label when icon-only
- ✅ Focus returns to input after sending
- ✅ Character counter linked via
aria-describedby
Related Skills
- implementing-chat-streaming - SSE streaming patterns and frontend state flow
- troubleshooting-authentication - MSAL popup issues and token debugging
- testing-with-playwright - Browser testing and accessibility validation