tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/python/rag/healthcare-support-portal/frontend/app/components/ui/textarea.tsx b/python/rag/healthcare-support-portal/frontend/app/components/ui/textarea.tsx
new file mode 100644
index 00000000..7f21b5e7
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/python/rag/healthcare-support-portal/frontend/app/components/ui/toaster.tsx b/python/rag/healthcare-support-portal/frontend/app/components/ui/toaster.tsx
new file mode 100644
index 00000000..ce50cb8c
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/components/ui/toaster.tsx
@@ -0,0 +1,25 @@
+import { Toaster as Sonner } from "sonner"
+
+type ToasterProps = React.ComponentProps
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ return (
+
+ )
+}
+
+export { Toaster }
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/components/ui/tooltip.tsx b/python/rag/healthcare-support-portal/frontend/app/components/ui/tooltip.tsx
new file mode 100644
index 00000000..71ee0fe1
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/components/ui/tooltip.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/python/rag/healthcare-support-portal/frontend/app/components/users/UserForm.tsx b/python/rag/healthcare-support-portal/frontend/app/components/users/UserForm.tsx
new file mode 100644
index 00000000..3d1e3961
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/components/users/UserForm.tsx
@@ -0,0 +1,160 @@
+import { useForm, getFormProps, getInputProps, getSelectProps } from '@conform-to/react';
+import { parseWithZod } from '@conform-to/zod';
+import { z } from 'zod';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { AlertCircle, Save, UserPlus } from 'lucide-react';
+import type { User } from '@/lib/types';
+
+const userSchema = z.object({
+ username: z.string().min(3, 'Username must be at least 3 characters'),
+ email: z.string().email('Invalid email address'),
+ password: z.string().min(8, 'Password must be at least 8 characters').optional(),
+ role: z.enum(['doctor', 'nurse', 'admin']),
+ department: z.enum(['cardiology', 'neurology', 'pediatrics', 'oncology', 'emergency', 'endocrinology', 'general']),
+});
+
+interface UserFormProps {
+ user?: User;
+ isEdit?: boolean;
+ error?: string;
+ departments?: string[];
+ roles?: string[];
+}
+
+export function UserForm({ user, isEdit = false, error, departments, roles }: UserFormProps) {
+ const [form, fields] = useForm({
+ defaultValue: {
+ username: user?.username || '',
+ email: user?.email || '',
+ role: user?.role || 'nurse',
+ department: user?.department || 'general',
+ password: '',
+ },
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: userSchema });
+ },
+ });
+
+ const defaultDepartments = [
+ 'cardiology', 'neurology', 'pediatrics', 'oncology',
+ 'emergency', 'endocrinology', 'obgyn', 'general'
+ ];
+
+ const defaultRoles = ['doctor', 'nurse', 'admin'];
+
+ const departmentList = departments || defaultDepartments;
+ const roleList = roles || defaultRoles;
+
+ return (
+
+
+
+
+ {isEdit ? 'Edit User' : 'Create New User'}
+
+
+ {isEdit
+ ? 'Update user account information and permissions'
+ : 'Add a new user to the Healthcare Support Portal'}
+
+
+
+ {error && (
+
+
+ {error}
+
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/lib/api.client.ts b/python/rag/healthcare-support-portal/frontend/app/lib/api.client.ts
new file mode 100644
index 00000000..d2cdd7f7
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/lib/api.client.ts
@@ -0,0 +1,50 @@
+import axios from 'axios';
+
+const API_BASE_URL = 'http://localhost';
+
+export const clientApi = {
+ async uploadDocument(formData: FormData, token: string) {
+ const response = await axios.post(`${API_BASE_URL}:8003/api/v1/documents/upload`, formData, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'multipart/form-data'
+ }
+ });
+ return response.data;
+ },
+
+ async searchDocuments(token: string, searchData: any) {
+ const response = await axios.post(`${API_BASE_URL}:8003/api/v1/chat/search`, searchData, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async askQuestion(token: string, questionData: any) {
+ const response = await axios.post(`${API_BASE_URL}:8003/api/v1/chat/ask`, questionData, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async getPatients(token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8002/api/v1/patients/`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async getDocuments(token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8003/api/v1/documents/`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async getCurrentUser(token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8001/api/v1/auth/me`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ }
+};
diff --git a/python/rag/healthcare-support-portal/frontend/app/lib/api.server.ts b/python/rag/healthcare-support-portal/frontend/app/lib/api.server.ts
new file mode 100644
index 00000000..44201fe5
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/lib/api.server.ts
@@ -0,0 +1,173 @@
+import axios from 'axios';
+
+const API_BASE_URL = 'http://localhost';
+
+export const serverApi = {
+ async login(credentials: { username: string; password: string }) {
+ // Create form data as the backend expects (username/password fields)
+ const formData = new URLSearchParams();
+ formData.append('username', credentials.username);
+ formData.append('password', credentials.password);
+
+ const response = await axios.post(`${API_BASE_URL}:8001/api/v1/auth/login`, formData, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+ });
+ return response.data;
+ },
+
+ async getCurrentUser(token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8001/api/v1/auth/me`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async getPatients(token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8002/api/v1/patients/`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async getPatient(patientId: number, token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8002/api/v1/patients/${patientId}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async getDocuments(token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8003/api/v1/documents/`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async getDocument(documentId: number, token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8003/api/v1/documents/${documentId}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async createPatient(token: string, patientData: any) {
+ const response = await axios.post(`${API_BASE_URL}:8002/api/v1/patients/`, patientData, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async updatePatient(patientId: number, patientData: any, token: string) {
+ const response = await axios.put(`${API_BASE_URL}:8002/api/v1/patients/${patientId}`, patientData, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async deletePatient(token: string, patientId: string) {
+ await axios.delete(`${API_BASE_URL}:8002/api/v1/patients/${patientId}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ },
+
+ async createDocument(documentData: any, token: string) {
+ const response = await axios.post(`${API_BASE_URL}:8003/api/v1/documents/`, documentData, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async updateDocument(token: string, documentId: string, documentData: any) {
+ const response = await axios.put(`${API_BASE_URL}:8003/api/v1/documents/${documentId}`, documentData, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async deleteDocument(token: string, documentId: string) {
+ await axios.delete(`${API_BASE_URL}:8003/api/v1/documents/${documentId}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ },
+
+ async getAllEmbeddingStatuses(token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8003/api/v1/documents/embedding-statuses`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async getEmbeddingStatus(documentId: number, token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8003/api/v1/documents/${documentId}/embedding-status`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async regenerateEmbeddings(documentId: number, token: string) {
+ const response = await axios.post(`${API_BASE_URL}:8003/api/v1/documents/${documentId}/regenerate-embeddings`, {}, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async uploadDocument(formData: FormData, token: string) {
+ const response = await axios.post(`${API_BASE_URL}:8003/api/v1/documents/upload`, formData, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'multipart/form-data'
+ }
+ });
+ return response.data;
+ },
+
+ async searchDocuments(token: string, searchData: any) {
+ const response = await axios.post(`${API_BASE_URL}:8003/api/v1/chat/search`, searchData, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async askQuestion(token: string, questionData: any) {
+ const response = await axios.post(`${API_BASE_URL}:8003/api/v1/chat/ask`, questionData, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async getUsers(token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8001/api/v1/users/`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async getUser(userId: string, token: string) {
+ const response = await axios.get(`${API_BASE_URL}:8001/api/v1/users/${userId}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async createUser(token: string, userData: any) {
+ const response = await axios.post(`${API_BASE_URL}:8001/api/v1/users/`, userData, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async updateUser(userId: number, userData: any, token: string) {
+ const response = await axios.put(`${API_BASE_URL}:8001/api/v1/users/${userId}`, userData, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ return response.data;
+ },
+
+ async deleteUser(token: string, userId: string) {
+ await axios.delete(`${API_BASE_URL}:8001/api/v1/users/${userId}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ }
+};
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/lib/api.ts b/python/rag/healthcare-support-portal/frontend/app/lib/api.ts
new file mode 100644
index 00000000..52a4d8df
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/lib/api.ts
@@ -0,0 +1,340 @@
+import axios from 'axios';
+import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
+import type {
+ User,
+ LoginRequest,
+ RegisterRequest,
+ AuthResponse,
+ Patient,
+ PatientCreate,
+ PatientUpdate,
+ Document,
+ DocumentCreate,
+ DocumentUpdate,
+ ChatRequest,
+ ChatResponse,
+ SearchRequest,
+ SearchResponse,
+ ApiError
+} from './types';
+
+class ApiClient {
+ private authApi: AxiosInstance;
+ private patientApi: AxiosInstance;
+ private ragApi: AxiosInstance;
+
+ constructor() {
+ const baseURL = import.meta.env.VITE_API_BASE_URL;
+ const authPort = import.meta.env.VITE_AUTH_SERVICE_PORT;
+ const patientPort = import.meta.env.VITE_PATIENT_SERVICE_PORT;
+ const ragPort = import.meta.env.VITE_RAG_SERVICE_PORT;
+
+ // Auth Service API
+ this.authApi = axios.create({
+ baseURL: `${baseURL}:${authPort}/api/v1`,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ // Patient Service API
+ this.patientApi = axios.create({
+ baseURL: `${baseURL}:${patientPort}/api/v1`,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ // RAG Service API
+ this.ragApi = axios.create({
+ baseURL: `${baseURL}:${ragPort}/api/v1`,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ // Add request interceptors to include auth tokens
+ this.setupInterceptors();
+ }
+
+ private setupInterceptors() {
+ const requestInterceptor = (config: any) => {
+ const token = localStorage.getItem('authToken');
+ if (token && config.headers) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ };
+
+ const responseInterceptor = (response: AxiosResponse) => response;
+
+ const errorInterceptor = (error: any) => {
+ if (error.response?.status === 401) {
+ // Token expired or invalid, clear auth and redirect to login
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('user');
+ window.location.href = '/login';
+ }
+ return Promise.reject(error);
+ };
+
+ // Apply interceptors to all API instances
+ [this.authApi, this.patientApi, this.ragApi].forEach(api => {
+ api.interceptors.request.use(requestInterceptor);
+ api.interceptors.response.use(responseInterceptor, errorInterceptor);
+ });
+ }
+
+ // Authentication Methods
+ async login(credentials: LoginRequest): Promise {
+ try {
+ const formData = new FormData();
+ formData.append('username', credentials.username);
+ formData.append('password', credentials.password);
+
+ const response = await this.authApi.post('/auth/login', formData, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ });
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async register(userData: RegisterRequest): Promise {
+ try {
+ const response = await this.authApi.post('/auth/register', userData);
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async getCurrentUser(): Promise {
+ try {
+ const response = await this.authApi.get('/auth/me');
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async refreshToken(): Promise {
+ try {
+ const response = await this.authApi.post('/auth/refresh');
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async getUsers(): Promise {
+ try {
+ const response = await this.authApi.get('/users/');
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ // Patient Methods
+ async getPatients(): Promise {
+ try {
+ const response = await this.patientApi.get('/patients/');
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async getPatient(id: number): Promise {
+ try {
+ const response = await this.patientApi.get(`/patients/${id}`);
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async createPatient(patientData: PatientCreate): Promise {
+ try {
+ const response = await this.patientApi.post('/patients/', patientData);
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async updatePatient(id: number, patientData: PatientUpdate): Promise {
+ try {
+ const response = await this.patientApi.put(`/patients/${id}`, patientData);
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async deletePatient(id: number): Promise {
+ try {
+ await this.patientApi.delete(`/patients/${id}`);
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ // Document Methods
+ async getDocuments(): Promise {
+ try {
+ const response = await this.ragApi.get('/documents/');
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async getDocument(id: number): Promise {
+ try {
+ const response = await this.ragApi.get(`/documents/${id}`);
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async createDocument(documentData: DocumentCreate): Promise {
+ try {
+ const response = await this.ragApi.post('/documents/', documentData);
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async updateDocument(id: number, documentData: DocumentUpdate): Promise {
+ try {
+ const response = await this.ragApi.put(`/documents/${id}`, documentData);
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async deleteDocument(id: number): Promise {
+ try {
+ await this.ragApi.delete(`/documents/${id}`);
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ // Chat and RAG Methods
+ async searchDocuments(searchRequest: SearchRequest): Promise {
+ try {
+ const response = await this.ragApi.post('/chat/search', searchRequest);
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async askQuestion(chatRequest: ChatRequest): Promise {
+ try {
+ const response = await this.ragApi.post('/chat/ask', chatRequest);
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async getConversationHistory(): Promise {
+ try {
+ const response = await this.ragApi.get('/chat/conversation-history');
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ async submitFeedback(responseId: string, rating: number, feedback?: string): Promise {
+ try {
+ const response = await this.ragApi.post('/chat/feedback', {
+ response_id: responseId,
+ rating,
+ feedback
+ });
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ // File Upload Method
+ async uploadFile(file: File, documentData: Partial): Promise {
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ // Add document metadata
+ Object.entries(documentData).forEach(([key, value]) => {
+ if (value !== undefined) {
+ formData.append(key, value.toString());
+ }
+ });
+
+ const response = await this.ragApi.post('/documents/upload', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+ return response.data;
+ } catch (error: any) {
+ throw this.handleError(error);
+ }
+ }
+
+ // Health Check Methods
+ async healthCheck(): Promise<{ [service: string]: any }> {
+ const results: { [service: string]: any } = {};
+
+ try {
+ const authHealth = await this.authApi.get('/health');
+ results.auth = authHealth.data;
+ } catch (error: any) {
+ results.auth = { status: 'unhealthy', error: error.message };
+ }
+
+ try {
+ const patientHealth = await this.patientApi.get('/health');
+ results.patient = patientHealth.data;
+ } catch (error: any) {
+ results.patient = { status: 'unhealthy', error: error.message };
+ }
+
+ try {
+ const ragHealth = await this.ragApi.get('/health');
+ results.rag = ragHealth.data;
+ } catch (error: any) {
+ results.rag = { status: 'unhealthy', error: error.message };
+ }
+
+ return results;
+ }
+
+ private handleError(error: any): never {
+ const apiError: ApiError = {
+ detail: error.response?.data?.detail || error.message || 'An error occurred',
+ status_code: error.response?.status || 500,
+ };
+ throw apiError;
+ }
+}
+
+// Create singleton instance
+export const api = new ApiClient();
+
+// Export individual service APIs for direct access if needed
+export const authApi = (api as any).authApi;
+export const patientApi = (api as any).patientApi;
+export const ragApi = (api as any).ragApi;
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/lib/auth.tsx b/python/rag/healthcare-support-portal/frontend/app/lib/auth.tsx
new file mode 100644
index 00000000..c6709a00
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/lib/auth.tsx
@@ -0,0 +1,246 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import type { ReactNode } from 'react';
+import type { User, LoginRequest, RegisterRequest, AuthContext } from './types';
+import { api } from './api';
+
+// Create Auth Context
+const AuthContextInstance = createContext(undefined);
+
+// Auth Provider Component
+interface AuthProviderProps {
+ children: ReactNode;
+}
+
+export function AuthProvider({ children }: AuthProviderProps) {
+ const [user, setUser] = useState(null);
+ const [token, setToken] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ // Initialize auth state from localStorage
+ useEffect(() => {
+ const initializeAuth = async () => {
+ try {
+ const savedToken = localStorage.getItem('authToken');
+ const savedUser = localStorage.getItem('user');
+
+ if (savedToken && savedUser) {
+ setToken(savedToken);
+ setUser(JSON.parse(savedUser));
+
+ // Verify token is still valid
+ try {
+ const currentUser = await api.getCurrentUser();
+ setUser(currentUser);
+ localStorage.setItem('user', JSON.stringify(currentUser));
+ } catch (error) {
+ // Token is invalid, clear auth
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('user');
+ setToken(null);
+ setUser(null);
+ }
+ }
+ } catch (error) {
+ console.error('Error initializing auth:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ initializeAuth();
+ }, []);
+
+ const login = async (credentials: LoginRequest) => {
+ try {
+ setIsLoading(true);
+ const authResponse = await api.login(credentials);
+ const token = authResponse.access_token;
+
+ // Store token
+ localStorage.setItem('authToken', token);
+ setToken(token);
+
+ // Get user information
+ const user = await api.getCurrentUser();
+ localStorage.setItem('user', JSON.stringify(user));
+ setUser(user);
+ } catch (error) {
+ throw error;
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const register = async (data: RegisterRequest) => {
+ try {
+ setIsLoading(true);
+ const user = await api.register(data);
+
+ // Auto-login after registration
+ await login({
+ username: data.username,
+ password: data.password
+ });
+ } catch (error) {
+ throw error;
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const logout = () => {
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('user');
+ setToken(null);
+ setUser(null);
+ };
+
+ const refreshToken = async () => {
+ try {
+ const authResponse = await api.refreshToken();
+ const newToken = authResponse.access_token;
+
+ localStorage.setItem('authToken', newToken);
+ setToken(newToken);
+
+ // Update user information
+ const user = await api.getCurrentUser();
+ localStorage.setItem('user', JSON.stringify(user));
+ setUser(user);
+ } catch (error) {
+ // If refresh fails, logout user
+ logout();
+ throw error;
+ }
+ };
+
+ const value: AuthContext = {
+ user,
+ token,
+ login,
+ register,
+ logout,
+ refreshToken,
+ isLoading,
+ isAuthenticated: !!token && !!user,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+// Hook to use auth context
+export function useAuth(): AuthContext {
+ const context = useContext(AuthContextInstance);
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+}
+
+// Helper functions for role-based access
+export function hasRole(user: User | null, roles: string[]): boolean {
+ if (!user) return false;
+ return roles.includes(user.role);
+}
+
+export function canAccessDepartment(user: User | null, department: string): boolean {
+ if (!user) return false;
+ if (user.role === 'admin') return true;
+ return user.department === department;
+}
+
+export function canManagePatients(user: User | null): boolean {
+ if (!user) return false;
+ return ['doctor', 'admin'].includes(user.role);
+}
+
+export function canViewSensitiveDocuments(user: User | null): boolean {
+ if (!user) return false;
+ return ['doctor', 'admin'].includes(user.role);
+}
+
+export function canCreateDocuments(user: User | null): boolean {
+ if (!user) return false;
+ return ['doctor', 'nurse', 'admin'].includes(user.role);
+}
+
+export function getUserDisplayName(user: User | null): string {
+ if (!user) return 'Unknown User';
+
+ const roleMap = {
+ doctor: 'Dr.',
+ nurse: 'Nurse',
+ admin: 'Admin'
+ };
+
+ const prefix = roleMap[user.role] || '';
+ return prefix ? `${prefix} ${user.username}` : user.username;
+}
+
+export function getRoleDisplayName(role: string): string {
+ const roleMap = {
+ doctor: 'Doctor',
+ nurse: 'Nurse',
+ admin: 'Administrator'
+ };
+
+ return roleMap[role as keyof typeof roleMap] || role;
+}
+
+export function getDepartmentDisplayName(department: string): string {
+ const departmentMap = {
+ cardiology: 'Cardiology',
+ neurology: 'Neurology',
+ pediatrics: 'Pediatrics',
+ oncology: 'Oncology',
+ emergency: 'Emergency Medicine',
+ endocrinology: 'Endocrinology',
+ general: 'General Medicine'
+ };
+
+ return departmentMap[department as keyof typeof departmentMap] || department;
+}
+
+// Token expiration checker
+export function isTokenExpired(token: string): boolean {
+ try {
+ const payload = JSON.parse(atob(token.split('.')[1]));
+ const currentTime = Date.now() / 1000;
+ return payload.exp < currentTime;
+ } catch (error) {
+ return true;
+ }
+}
+
+// Auto token refresh setup
+export function setupTokenRefresh() {
+ const token = localStorage.getItem('authToken');
+ if (!token) return;
+
+ try {
+ const payload = JSON.parse(atob(token.split('.')[1]));
+ const expirationTime = payload.exp * 1000;
+ const currentTime = Date.now();
+ const timeUntilExpiration = expirationTime - currentTime;
+
+ // Refresh token 5 minutes before expiration
+ const refreshTime = Math.max(timeUntilExpiration - 5 * 60 * 1000, 0);
+
+ if (refreshTime > 0) {
+ setTimeout(() => {
+ api.refreshToken().catch(() => {
+ // If refresh fails, logout user
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('user');
+ window.location.href = '/login';
+ });
+ }, refreshTime);
+ }
+ } catch (error) {
+ console.error('Error setting up token refresh:', error);
+ }
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/lib/schemas.ts b/python/rag/healthcare-support-portal/frontend/app/lib/schemas.ts
new file mode 100644
index 00000000..c50a99d7
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/lib/schemas.ts
@@ -0,0 +1,92 @@
+import { z } from 'zod';
+
+export const loginSchema = z.object({
+ username: z.string().min(1, 'Username is required'),
+ password: z.string().min(1, 'Password is required'),
+});
+
+export const userCreateSchema = z.object({
+ username: z.string().min(3, 'Username must be at least 3 characters'),
+ email: z.string().email('Invalid email address'),
+ password: z.string().min(8, 'Password must be at least 8 characters'),
+ role: z.enum(['doctor', 'nurse', 'admin']),
+ department: z.enum(['cardiology', 'neurology', 'pediatrics', 'oncology', 'emergency', 'endocrinology', 'obgyn', 'general']),
+});
+
+export const userUpdateSchema = userCreateSchema.partial().omit({ password: true });
+
+export const patientCreateSchema = z.object({
+ name: z.string().min(1, 'Name is required'),
+ date_of_birth: z.string().optional(),
+ medical_record_number: z.string().min(1, 'Medical record number is required'),
+ department: z.enum(['cardiology', 'neurology', 'pediatrics', 'oncology', 'emergency', 'endocrinology', 'obgyn', 'general']),
+ assigned_doctor_id: z.string().optional().transform(val => {
+ if (!val || val === '' || val === 'none') return undefined;
+ const num = parseInt(val);
+ return isNaN(num) ? undefined : num;
+ }),
+});
+
+export const patientUpdateSchema = patientCreateSchema.partial();
+
+export const documentCreateSchema = z.object({
+ title: z.string().min(1, 'Title is required'),
+ content: z.string().min(1, 'Content is required'),
+ document_type: z.enum(['protocol', 'policy', 'guideline', 'research', 'report', 'medical_record']),
+ patient_id: z.string().optional().transform(val => {
+ if (!val || val === '' || val === 'none') return undefined;
+ const num = parseInt(val);
+ return isNaN(num) ? undefined : num;
+ }),
+ department: z.enum(['cardiology', 'neurology', 'pediatrics', 'oncology', 'emergency', 'endocrinology', 'obgyn', 'general']),
+ is_sensitive: z.string().transform(val => val === 'true').default('false'),
+});
+
+export const documentUpdateSchema = documentCreateSchema.partial();
+
+// Chat/RAG Schemas
+export const chatMessageSchema = z.object({
+ message: z.string()
+ .min(1, 'Message is required')
+ .max(1000, 'Message must be less than 1000 characters'),
+});
+
+export const chatRequestSchema = z.object({
+ message: z.string()
+ .min(1, 'Message is required')
+ .max(1000, 'Message must be less than 1000 characters'),
+ context_department: z.string().optional(),
+ max_results: z.number()
+ .int()
+ .min(1)
+ .max(10)
+ .optional()
+ .default(5),
+});
+
+// Search Schema
+export const searchRequestSchema = z.object({
+ query: z.string()
+ .min(1, 'Search query is required')
+ .max(200, 'Search query must be less than 200 characters'),
+ department: z.string().optional(),
+ document_type: z.string().optional(),
+ limit: z.number()
+ .int()
+ .min(1)
+ .max(50)
+ .optional()
+ .default(10),
+});
+
+// Type exports
+export type LoginInput = z.infer;
+export type UserCreateInput = z.infer;
+export type UserUpdateInput = z.infer;
+export type PatientCreateInput = z.infer;
+export type PatientUpdateInput = z.infer;
+export type DocumentCreateInput = z.infer;
+export type DocumentUpdateInput = z.infer;
+export type ChatMessageInput = z.infer;
+export type ChatRequestInput = z.infer;
+export type SearchRequestInput = z.infer;
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/lib/types.ts b/python/rag/healthcare-support-portal/frontend/app/lib/types.ts
new file mode 100644
index 00000000..c10ba623
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/lib/types.ts
@@ -0,0 +1,228 @@
+// User and Authentication Types
+export interface User {
+ id: number;
+ username: string;
+ email: string;
+ role: 'doctor' | 'nurse' | 'admin';
+ department: string;
+ is_active: boolean;
+ created_at: string;
+}
+
+export interface LoginRequest {
+ username: string;
+ password: string;
+}
+
+export interface RegisterRequest {
+ username: string;
+ email: string;
+ password: string;
+ role: 'doctor' | 'nurse' | 'admin';
+ department: string;
+}
+
+export interface AuthResponse {
+ access_token: string;
+ token_type: string;
+}
+
+export interface AuthContext {
+ user: User | null;
+ token: string | null;
+ login: (credentials: LoginRequest) => Promise;
+ register: (data: RegisterRequest) => Promise;
+ logout: () => void;
+ refreshToken: () => Promise;
+ isLoading: boolean;
+ isAuthenticated: boolean;
+}
+
+// Patient Types
+export interface Patient {
+ id: number;
+ name: string;
+ date_of_birth: string | null;
+ medical_record_number: string;
+ department: string;
+ assigned_doctor_id: number | null;
+ is_active: boolean;
+ created_at: string;
+ assigned_doctor?: User;
+}
+
+export interface PatientCreate {
+ name: string;
+ date_of_birth?: string;
+ medical_record_number: string;
+ department: string;
+ assigned_doctor_id?: number;
+}
+
+export interface PatientUpdate extends Partial {
+ is_active?: boolean;
+}
+
+// Document Types
+export interface Document {
+ id: number;
+ title: string;
+ content: string;
+ document_type: string;
+ patient_id: number | null;
+ department: string;
+ created_by_id: number;
+ is_sensitive: boolean;
+ created_at: string;
+ patient?: Patient;
+ created_by?: User;
+}
+
+export interface DocumentCreate {
+ title: string;
+ content: string;
+ document_type: string;
+ patient_id?: number;
+ department: string;
+ is_sensitive?: boolean;
+}
+
+export interface DocumentUpdate extends Partial {}
+
+// Chat and RAG Types
+export interface ChatMessage {
+ id: string;
+ type: 'user' | 'assistant' | 'system';
+ content: string;
+ timestamp: string;
+ sources?: SearchResult[];
+ token_count?: number;
+}
+
+export interface ChatRequest {
+ message: string;
+ context_patient_id?: number;
+ context_department?: string;
+ max_results?: number;
+}
+
+export interface ChatResponse {
+ response: string;
+ sources: SearchResult[];
+ token_count: number;
+ context_used: boolean;
+}
+
+export interface SearchRequest {
+ query: string;
+ document_types?: string[];
+ department?: string;
+ limit?: number;
+}
+
+export interface SearchResponse {
+ results: SearchResult[];
+ total_results: number;
+}
+
+export interface SearchResult {
+ id: number;
+ title: string;
+ content_chunk: string;
+ document_type: string;
+ department: string;
+ similarity_score: number;
+ created_at: string;
+}
+
+// API Response Types
+export interface ApiResponse {
+ data: T;
+ message?: string;
+ status: number;
+}
+
+export interface ApiError {
+ detail: string;
+ status_code: number;
+}
+
+export interface PaginatedResponse {
+ items: T[];
+ total: number;
+ page: number;
+ per_page: number;
+ pages: number;
+}
+
+// Form Types
+export interface FormField {
+ label: string;
+ name: string;
+ type: 'text' | 'email' | 'password' | 'select' | 'textarea' | 'date' | 'checkbox';
+ placeholder?: string;
+ required?: boolean;
+ options?: { label: string; value: string }[];
+ validation?: any; // Zod schema
+}
+
+// UI Component Types
+export interface NavItem {
+ label: string;
+ href: string;
+ icon?: any;
+ roles?: string[];
+ children?: NavItem[];
+}
+
+export interface TableColumn {
+ key: keyof T;
+ label: string;
+ sortable?: boolean;
+ render?: (value: any, item: T) => React.ReactNode;
+}
+
+export interface AlertProps {
+ type: 'success' | 'error' | 'warning' | 'info';
+ title?: string;
+ message: string;
+ dismissible?: boolean;
+}
+
+// Utility Types
+export type Role = 'doctor' | 'nurse' | 'admin';
+
+export type Department =
+ | 'cardiology'
+ | 'neurology'
+ | 'pediatrics'
+ | 'oncology'
+ | 'emergency'
+ | 'endocrinology'
+ | 'obgyn'
+ | 'general';
+
+export type DocumentType =
+ | 'medical_record'
+ | 'protocol'
+ | 'policy'
+ | 'guideline'
+ | 'research'
+ | 'report';
+
+export type Status = 'active' | 'inactive' | 'pending' | 'urgent';
+
+// Environment Variables
+export interface ImportMetaEnv {
+ readonly VITE_API_BASE_URL: string;
+ readonly VITE_AUTH_SERVICE_PORT: string;
+ readonly VITE_PATIENT_SERVICE_PORT: string;
+ readonly VITE_RAG_SERVICE_PORT: string;
+ readonly VITE_APP_NAME: string;
+ readonly VITE_APP_VERSION: string;
+ readonly VITE_DEBUG: string;
+}
+
+export interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/lib/utils.ts b/python/rag/healthcare-support-portal/frontend/app/lib/utils.ts
new file mode 100644
index 00000000..db2ea2fd
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/lib/utils.ts
@@ -0,0 +1,124 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
+
+export function formatDate(date: string | Date): string {
+ const d = new Date(date)
+ return d.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ })
+}
+
+export function formatDateTime(date: string | Date): string {
+ const d = new Date(date)
+ return d.toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+}
+
+export function getRoleColor(role: string): string {
+ switch (role.toLowerCase()) {
+ case 'doctor':
+ return 'text-blue-600 bg-blue-50 border-blue-200'
+ case 'nurse':
+ return 'text-green-600 bg-green-50 border-green-200'
+ case 'admin':
+ return 'text-purple-600 bg-purple-50 border-purple-200'
+ default:
+ return 'text-gray-600 bg-gray-50 border-gray-200'
+ }
+}
+
+export function getDepartmentColor(department: string): string {
+ switch (department.toLowerCase()) {
+ case 'cardiology':
+ return 'border-red-500'
+ case 'neurology':
+ return 'border-purple-500'
+ case 'pediatrics':
+ return 'border-yellow-500'
+ case 'oncology':
+ return 'border-pink-500'
+ case 'emergency':
+ return 'border-orange-500'
+ case 'endocrinology':
+ return 'border-teal-500'
+ default:
+ return 'border-blue-500'
+ }
+}
+
+export function getStatusColor(status: string): string {
+ switch (status.toLowerCase()) {
+ case 'active':
+ return 'text-green-600 bg-green-50 border-green-200'
+ case 'inactive':
+ return 'text-gray-600 bg-gray-50 border-gray-200'
+ case 'pending':
+ return 'text-yellow-600 bg-yellow-50 border-yellow-200'
+ case 'urgent':
+ return 'text-red-600 bg-red-50 border-red-200'
+ default:
+ return 'text-gray-600 bg-gray-50 border-gray-200'
+ }
+}
+
+export function truncateText(text: string, maxLength: number): string {
+ if (text.length <= maxLength) return text
+ return text.slice(0, maxLength) + '...'
+}
+
+export function generateInitials(name: string): string {
+ return name
+ .split(' ')
+ .map(word => word.charAt(0))
+ .join('')
+ .toUpperCase()
+ .slice(0, 2)
+}
+
+export function validateEmail(email: string): boolean {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ return emailRegex.test(email)
+}
+
+export function debounce any>(
+ func: T,
+ wait: number
+): (...args: Parameters) => void {
+ let timeout: NodeJS.Timeout | null = null
+
+ return (...args: Parameters) => {
+ if (timeout) clearTimeout(timeout)
+ timeout = setTimeout(() => func(...args), wait)
+ }
+}
+
+export function formatFileSize(bytes: number): string {
+ if (bytes === 0) return '0 Bytes'
+
+ const k = 1024
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+}
+
+export function isValidMRN(mrn: string): boolean {
+ // Basic MRN validation - adjust pattern as needed
+ const mrnRegex = /^MRN-\d{4}-\d{3}$/
+ return mrnRegex.test(mrn)
+}
+
+export function sanitizeInput(input: string): string {
+ return input.trim().replace(/[<>]/g, '')
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/lib/utils/action-utils.ts b/python/rag/healthcare-support-portal/frontend/app/lib/utils/action-utils.ts
new file mode 100644
index 00000000..d3f9df14
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/lib/utils/action-utils.ts
@@ -0,0 +1,62 @@
+import { parseWithZod } from '@conform-to/zod';
+import { redirect } from 'react-router';
+import { json } from './loader-utils';
+
+export async function handleFormSubmission(
+ request: Request,
+ schema: any,
+ handler: (data: any) => Promise
+) {
+ try {
+ const formData = await request.formData();
+ const submission = parseWithZod(formData, { schema });
+
+ if (submission.status !== 'success') {
+ return json({
+ submission: submission.reply(),
+ }, { status: 400 });
+ }
+
+ return await handler(submission.value);
+ } catch (error) {
+ console.error('Form submission error:', error);
+ return json({
+ submission: {
+ formErrors: ['An unexpected error occurred. Please try again.'],
+ },
+ }, { status: 500 });
+ }
+}
+
+export function setAuthCookies(response: Response, token: string, user: any): Response {
+ // Set HTTP-only cookies for authentication
+ const headers = new Headers(response.headers);
+
+ // Set auth token cookie
+ headers.append('Set-Cookie', `authToken=${token}; HttpOnly; Path=/; SameSite=Strict; Max-Age=1800`);
+
+ // Set user info cookie (for client-side access)
+ headers.append('Set-Cookie', `userInfo=${JSON.stringify(user)}; Path=/; SameSite=Strict; Max-Age=1800`);
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers,
+ });
+}
+
+export function clearAuthCookies(): Response {
+ const headers = new Headers();
+
+ // Clear auth cookies
+ headers.append('Set-Cookie', 'authToken=; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT');
+ headers.append('Set-Cookie', 'userInfo=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT');
+
+ return new Response(null, {
+ status: 302,
+ headers: {
+ ...Object.fromEntries(headers.entries()),
+ Location: '/login',
+ },
+ });
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/lib/utils/loader-utils.ts b/python/rag/healthcare-support-portal/frontend/app/lib/utils/loader-utils.ts
new file mode 100644
index 00000000..b11bffaa
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/lib/utils/loader-utils.ts
@@ -0,0 +1,82 @@
+import { redirect } from 'react-router';
+import { serverApi } from '@/lib/api.server';
+
+export interface User {
+ id: number;
+ username: string;
+ email: string;
+ role: string;
+ department: string;
+ is_active: boolean;
+ created_at: string;
+}
+
+export async function getCurrentUser(request: Request): Promise {
+ try {
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ return null;
+ }
+
+ // Verify token by calling auth service
+ const user = await serverApi.getCurrentUser(token);
+ return user;
+ } catch (error) {
+ console.error('Failed to get current user:', error);
+ return null;
+ }
+}
+
+export function json(data: any, init?: ResponseInit) {
+ return new Response(JSON.stringify(data), {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...init?.headers,
+ },
+ ...init,
+ });
+}
+
+export function getAuthToken(request: Request): string | null {
+ const cookieHeader = request.headers.get('Cookie');
+ return cookieHeader?.match(/authToken=([^;]+)/)?.[1] || null;
+}
+
+export async function requireAuth(request: Request): Promise {
+ const user = await getCurrentUser(request);
+
+ if (!user) {
+ throw new Response('Authentication required', {
+ status: 401,
+ statusText: 'Unauthorized'
+ });
+ }
+
+ return user;
+}
+
+export function handleApiError(error: any): Response {
+ console.error('API Error:', error);
+
+ // If it's already a Response, return it
+ if (error instanceof Response) {
+ return error;
+ }
+
+ // If it's an error with status, create appropriate response
+ if (error?.status) {
+ return new Response(error.message || 'API Error', {
+ status: error.status,
+ statusText: error.statusText || 'Error'
+ });
+ }
+
+ // Default error response
+ return new Response(
+ error?.message || 'An unexpected error occurred',
+ { status: 500, statusText: 'Internal Server Error' }
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/root.tsx b/python/rag/healthcare-support-portal/frontend/app/root.tsx
new file mode 100644
index 00000000..914ca72d
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/root.tsx
@@ -0,0 +1,76 @@
+import {
+ isRouteErrorResponse,
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "react-router";
+import type { LinksFunction } from "react-router";
+
+import type { Route } from "./+types/root";
+import "./styles/globals.css";
+
+export const links: LinksFunction = () => [
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
+ {
+ rel: "preconnect",
+ href: "https://fonts.gstatic.com",
+ crossOrigin: "anonymous",
+ },
+ {
+ rel: "stylesheet",
+ href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
+ },
+];
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export default function App() {
+ return ;
+}
+
+export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
+ let message = "Oops!";
+ let details = "An unexpected error occurred.";
+ let stack: string | undefined;
+
+ if (isRouteErrorResponse(error)) {
+ message = error.status === 404 ? "404" : "Error";
+ details =
+ error.status === 404
+ ? "The requested page could not be found."
+ : error.statusText || details;
+ } else if (import.meta.env.DEV && error && error instanceof Error) {
+ details = error.message;
+ stack = error.stack;
+ }
+
+ return (
+
+ {message}
+ {details}
+ {stack && (
+
+ {stack}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes.ts b/python/rag/healthcare-support-portal/frontend/app/routes.ts
new file mode 100644
index 00000000..8e292484
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes.ts
@@ -0,0 +1,22 @@
+import { type RouteConfig, route, index, layout } from "@react-router/dev/routes";
+
+export default [
+ layout("routes/_layout.tsx", [
+ index("routes/_index.tsx"),
+ route("login", "routes/login.tsx"),
+ route("demo-login", "routes/demo-login.tsx"),
+ route("logout", "routes/logout.tsx"),
+ route("patients", "routes/patients/index.tsx"),
+ route("patients/new", "routes/patients/new.tsx"),
+ route("patients/:id", "routes/patients/patient.tsx"),
+ route("patients/:id/edit", "routes/patients/$id.edit.tsx"),
+ route("chat", "routes/chat.tsx"),
+ route("documents", "routes/documents.tsx"),
+ route("documents/new", "routes/documents/new.tsx"),
+ route("documents/:id", "routes/documents/$id.tsx"),
+ route("users", "routes/users/index.tsx"),
+ route("users/new", "routes/users/new.tsx"),
+ route("users/:id/edit", "routes/users/$id.edit.tsx"),
+ route("settings", "routes/settings.tsx"),
+ ]),
+] satisfies RouteConfig;
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/_index.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/_index.tsx
new file mode 100644
index 00000000..600f8a9a
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/_index.tsx
@@ -0,0 +1,459 @@
+import { useState, useEffect } from 'react';
+import { Link, useLoaderData } from 'react-router';
+import {
+ Users,
+ FileText,
+ MessageCircle,
+ Activity,
+ Plus,
+ Eye,
+ TrendingUp,
+ Calendar,
+ Bell,
+ Search
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { formatDateTime, formatDate } from '@/lib/utils';
+import { getCurrentUser, handleApiError } from '@/lib/utils/loader-utils';
+import { serverApi } from '@/lib/api.server';
+import type { User, Patient, Document } from '@/lib/types';
+import type { LoaderFunctionArgs } from 'react-router';
+
+interface DashboardData {
+ user: User;
+ patients: Patient[];
+ documents: Document[];
+ recentDocuments: Document[];
+ myPatients: Patient[];
+ departmentStats: {
+ totalPatients: number;
+ totalDocuments: number;
+ activePatients: number;
+ sensitiveDocuments: number;
+ };
+}
+
+// Loader function - fetch dashboard data
+export async function loader({ request }: LoaderFunctionArgs) {
+ try {
+ // Get current user - layout already ensures authentication
+ const user = await getCurrentUser(request);
+ if (!user) {
+ console.error('[Dashboard] No user found in loader - layout auth may have failed');
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ console.error('[Dashboard] No auth token found');
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ console.log(`[Dashboard] Loading dashboard for ${user.username}`);
+
+ // Load data concurrently based on user role
+ const [patients, documents] = await Promise.all([
+ serverApi.getPatients(token).catch(error => {
+ console.error('[Dashboard] Failed to load patients:', error);
+ return [];
+ }),
+ serverApi.getDocuments(token).catch(error => {
+ console.error('[Dashboard] Failed to load documents:', error);
+ return [];
+ })
+ ]);
+
+ // Process data for dashboard
+ const myPatients = user.role === 'doctor'
+ ? patients.filter((p: Patient) => p.assigned_doctor_id === user.id)
+ : patients.filter((p: Patient) => p.department === user.department);
+
+ const recentDocuments = documents
+ .sort((a: Document, b: Document) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
+ .slice(0, 5);
+
+ const departmentStats = {
+ totalPatients: patients.length,
+ totalDocuments: documents.length,
+ activePatients: patients.filter((p: Patient) => p.is_active).length,
+ sensitiveDocuments: documents.filter((d: Document) => d.is_sensitive).length,
+ };
+
+ console.log(`[Dashboard] Loaded ${patients.length} patients, ${documents.length} documents`);
+
+ return {
+ user,
+ patients,
+ documents,
+ recentDocuments,
+ myPatients,
+ departmentStats
+ };
+ } catch (error) {
+ console.error('[Dashboard] Loader error:', error);
+ throw handleApiError(error);
+ }
+}
+
+export default function Dashboard() {
+ const { user, patients, documents, recentDocuments, myPatients, departmentStats } = useLoaderData();
+ const [currentTime, setCurrentTime] = useState(new Date());
+
+ // Update current time every minute
+ useEffect(() => {
+ const timer = setInterval(() => setCurrentTime(new Date()), 60000);
+ return () => clearInterval(timer);
+ }, []);
+
+ const getGreeting = () => {
+ const hour = currentTime.getHours();
+ if (hour < 12) return 'Good morning';
+ if (hour < 17) return 'Good afternoon';
+ return 'Good evening';
+ };
+
+ const getRoleTitle = (role: string) => {
+ switch (role) {
+ case 'doctor': return 'Dr.';
+ case 'nurse': return 'Nurse';
+ case 'admin': return 'Administrator';
+ default: return '';
+ }
+ };
+
+ const formatDisplayName = (username: string, role: string) => {
+ // Handle known demo users
+ if (username === 'dr_smith') return 'Smith';
+ if (username === 'nurse_johnson') return 'Johnson';
+ if (username === 'admin_wilson') return 'Wilson';
+
+ // Remove role prefixes to avoid duplication (dr_, nurse_, admin_)
+ let cleanUsername = username;
+ if (username.startsWith('dr_')) {
+ cleanUsername = username.substring(3); // Remove 'dr_'
+ } else if (username.startsWith('nurse_')) {
+ cleanUsername = username.substring(6); // Remove 'nurse_'
+ } else if (username.startsWith('admin_')) {
+ cleanUsername = username.substring(6); // Remove 'admin_'
+ }
+
+ // For other usernames, extract last part after underscore and capitalize
+ const parts = cleanUsername.split('_');
+ if (parts.length > 1) {
+ const lastName = parts[parts.length - 1];
+ return lastName.charAt(0).toUpperCase() + lastName.slice(1);
+ }
+
+ // Fallback: capitalize the clean username
+ return cleanUsername.charAt(0).toUpperCase() + cleanUsername.slice(1);
+ };
+
+ const getDocumentIcon = (type: string) => {
+ switch (type) {
+ case 'protocol': return '📋';
+ case 'policy': return '📜';
+ case 'guideline': return '📖';
+ case 'research': return '🔬';
+ case 'report': return '📊';
+ case 'medical_record': return '📝';
+ default: return '📄';
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+ {getGreeting()}, {getRoleTitle(user.role)} {formatDisplayName(user.username, user.role)}
+
+
+ {formatDateTime(currentTime.toISOString())} • {user.department} Department
+
+
+
+
+
+
+ AI Assistant
+
+
+
+
+
+ New Document
+
+
+
+
+
+ {/* Quick Stats */}
+
+
+
+
+
+
+
+
+
+ {user.role === 'doctor' ? 'My Patients' : 'Department Patients'}
+
+
+ {user.role === 'doctor' ? myPatients.length : departmentStats.activePatients}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Documents
+
+ {departmentStats.totalDocuments}
+
+
+
+
+
+
+
+
+
+
+
+
Active Cases
+
+ {departmentStats.activePatients}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sensitive Docs
+
+ {departmentStats.sensitiveDocuments}
+
+
+
+
+
+
+
+
+ {/* Recent Documents */}
+
+
+
+
+ Recent Documents
+
+
+ Latest documents in your department
+
+
+
+ {recentDocuments.length === 0 ? (
+
+
+
No documents yet
+
Get started by creating your first document.
+
+
+
+
+ Create Document
+
+
+
+
+ ) : (
+
+ {recentDocuments.map((doc) => (
+
+
+ {getDocumentIcon(doc.document_type)}
+
+
+
+ {doc.title}
+
+
+
+ {doc.document_type.replace('_', ' ')}
+
+ {doc.is_sensitive && (
+
+ Sensitive
+
+ )}
+
+
+ {formatDate(doc.created_at)}
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ View All Documents
+
+
+
+
+ )}
+
+
+
+ {/* My Patients or Department Patients */}
+
+
+
+
+ {user.role === 'doctor' ? 'My Patients' : 'Department Patients'}
+
+
+ {user.role === 'doctor'
+ ? 'Patients assigned to you'
+ : `Patients in ${user.department} department`
+ }
+
+
+
+ {myPatients.length === 0 ? (
+
+
+
No patients yet
+
+ {user.role === 'doctor'
+ ? 'No patients are currently assigned to you.'
+ : 'No patients in your department yet.'
+ }
+
+ {['doctor', 'admin'].includes(user.role) && (
+
+
+
+
+ Add Patient
+
+
+
+ )}
+
+ ) : (
+
+ {myPatients.slice(0, 5).map((patient) => (
+
+
+
+
+
+
+ {patient.name}
+
+
+ {patient.medical_record_number}
+
+ {patient.date_of_birth && (
+
+ DOB: {formatDate(patient.date_of_birth)}
+
+ )}
+
+
+
+
+
+
+
+ ))}
+
+
+
+ View All Patients
+
+
+
+
+ )}
+
+
+
+
+ {/* Quick Actions */}
+
+
+
+
+ Quick Actions
+
+
+ Common tasks for healthcare professionals
+
+
+
+
+
+
+
+ AI Chat
+
+
+
+
+
+
+ New Document
+
+
+
+ {['doctor', 'admin'].includes(user.role) && (
+
+
+
+ Add Patient
+
+
+ )}
+
+
+
+
+ Search Docs
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/_layout.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/_layout.tsx
new file mode 100644
index 00000000..49a17408
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/_layout.tsx
@@ -0,0 +1,87 @@
+import { Outlet, useLoaderData, redirect } from "react-router";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { Navigation } from "@/components/Navigation";
+import { Toaster } from "@/components/ui/toaster";
+import { getCurrentUser } from "@/lib/utils/loader-utils";
+import type { User } from "@/lib/types";
+import type { LoaderFunctionArgs } from 'react-router';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: 1,
+ refetchOnWindowFocus: false,
+ },
+ },
+});
+
+// Loader function - check authentication and provide user data
+export async function loader({ request }: LoaderFunctionArgs) {
+ const url = new URL(request.url);
+ const pathname = url.pathname;
+
+ console.log(`[Layout] Loading ${pathname}`);
+
+ // Define public routes that don't require authentication
+ const publicRoutes = ['/login', '/signup'];
+ const isPublicRoute = publicRoutes.includes(pathname);
+
+ const user = await getCurrentUser(request);
+
+ console.log(`[Layout] User: ${user ? `${user.username} (${user.role})` : 'none'}, Public route: ${isPublicRoute}`);
+
+ // If not authenticated and trying to access protected route, redirect to login
+ if (!user && !isPublicRoute) {
+ console.log(`[Layout] Redirecting to login: no user for protected route ${pathname}`);
+ throw redirect('/login');
+ }
+
+ // If authenticated and trying to access auth pages, redirect to dashboard
+ if (user && isPublicRoute) {
+ console.log(`[Layout] Redirecting to dashboard: authenticated user accessing ${pathname}`);
+ throw redirect('/');
+ }
+
+ console.log(`[Layout] Allowing access to ${pathname}`);
+ return { user, isPublicRoute };
+}
+
+interface LayoutData {
+ user: User | null;
+ isPublicRoute: boolean;
+}
+
+function LayoutContent() {
+ const { user, isPublicRoute } = useLoaderData();
+
+ // Render layout for authenticated users
+ if (user && !isPublicRoute) {
+ return (
+
+ );
+ }
+
+ // Render auth pages without navigation
+ return (
+
+
+
+
+ );
+}
+
+export default function Layout() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/chat.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/chat.tsx
new file mode 100644
index 00000000..6807f0e8
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/chat.tsx
@@ -0,0 +1,365 @@
+import { useState, useRef, useEffect } from 'react';
+import { Send, MessageSquare, Loader2, FileText, User as UserIcon, Bot } from 'lucide-react';
+import { useLoaderData, useFetcher, Form } from 'react-router';
+import { useForm, getFormProps, getInputProps } from '@conform-to/react';
+import { parseWithZod } from '@conform-to/zod';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import rehypeHighlight from 'rehype-highlight';
+import rehypeSanitize from 'rehype-sanitize';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { formatDateTime } from '@/lib/utils';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { handleFormSubmission } from '@/lib/utils/action-utils';
+import { serverApi } from '@/lib/api.server';
+import { chatMessageSchema } from '@/lib/schemas';
+import type { ChatMessage, ChatRequest, User } from '@/lib/types';
+import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
+
+interface ChatData {
+ user: User;
+}
+
+// Loader function - handle authentication
+export async function loader({ request }: LoaderFunctionArgs) {
+ try {
+ // Require authentication
+ const user = await requireAuth(request);
+
+ return {
+ user
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+// Action function - handle message submissions
+export async function action({ request }: ActionFunctionArgs) {
+ const user = await requireAuth(request);
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ return handleFormSubmission(request, chatMessageSchema, async (data) => {
+ const chatRequest: ChatRequest = {
+ message: data.message,
+ context_department: user.department,
+ max_results: 5
+ };
+
+ const response = await serverApi.askQuestion(token, chatRequest);
+
+ // Return the response for the fetcher
+ return Response.json({
+ success: true,
+ response: response.response,
+ sources: response.sources,
+ token_count: response.token_count
+ });
+ });
+}
+
+export default function Chat() {
+ const { user } = useLoaderData();
+ const fetcher = useFetcher();
+ const [messages, setMessages] = useState([]);
+ const messagesEndRef = useRef(null);
+ const [form, fields] = useForm({
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: chatMessageSchema });
+ }
+ });
+
+ const isLoading = fetcher.state === 'submitting';
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ };
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messages]);
+
+ useEffect(() => {
+ // Add welcome message
+ const welcomeMessage: ChatMessage = {
+ id: 'welcome',
+ type: 'system',
+ content: `Welcome to the Healthcare Support Portal Chat Assistant! I'm here to help you with medical information, protocols, and answer questions based on your available documents. How can I assist you today?`,
+ timestamp: new Date().toISOString()
+ };
+ setMessages([welcomeMessage]);
+ }, []);
+
+ useEffect(() => {
+ // Handle fetcher response
+ if (fetcher.data && fetcher.state === 'idle') {
+ if (fetcher.data.success) {
+ const assistantMessage: ChatMessage = {
+ id: (Date.now() + 1).toString(),
+ type: 'assistant',
+ content: fetcher.data.response,
+ timestamp: new Date().toISOString(),
+ sources: fetcher.data.sources,
+ token_count: fetcher.data.token_count
+ };
+ setMessages(prev => [...prev, assistantMessage]);
+ } else {
+ const errorMessage: ChatMessage = {
+ id: (Date.now() + 1).toString(),
+ type: 'assistant',
+ content: `I apologize, but I encountered an error while processing your request. Please try again later.`,
+ timestamp: new Date().toISOString()
+ };
+ setMessages(prev => [...prev, errorMessage]);
+ }
+ }
+ }, [fetcher.data, fetcher.state]);
+
+ const handleSendMessage = (formData: FormData) => {
+ const message = formData.get('message') as string;
+ if (!message?.trim() || isLoading) return;
+
+ const userMessage: ChatMessage = {
+ id: Date.now().toString(),
+ type: 'user',
+ content: message.trim(),
+ timestamp: new Date().toISOString()
+ };
+
+ setMessages(prev => [...prev, userMessage]);
+
+ // Submit via fetcher
+ fetcher.submit(formData, { method: 'post' });
+
+ // Reset form
+ form.reset();
+ };
+
+ const renderMessage = (message: ChatMessage) => {
+ switch (message.type) {
+ case 'user':
+ return (
+
+
+
+ {message.content}
+
+
+
+
+ );
+
+ case 'assistant':
+ return (
+
+
+
+
+
+
+ {message.content}
+
+
+ {message.sources && message.sources.length > 0 && (
+
+
Sources:
+
+ {message.sources.slice(0, 3).map((source, idx) => (
+
+
+ {source.title}
+
+ ))}
+
+
+ )}
+ {message.token_count && (
+
+ {message.token_count} tokens used
+
+ )}
+
+
+
+ );
+
+ case 'system':
+ return (
+
+
+ {message.content}
+
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ const suggestedQuestions = [
+ "What are the latest diabetes management protocols?",
+ "Show me emergency procedures for cardiac events",
+ "What are the medication guidelines for pediatric patients?",
+ "How do I handle patient data privacy?",
+ "What are the current vaccination schedules?"
+ ];
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Chat Assistant
+
+
+ Get AI-powered assistance with medical information and protocols
+
+
+
+
+ Context: {user.department}
+
+
+
+
+
+ {/* Chat Container */}
+
+
+
+
+ Healthcare AI Assistant
+
+
+ Ask questions about medical protocols, patient care, or search through available documents
+
+
+
+
+ {/* Messages Area */}
+
+ {messages.length === 1 && (
+
+ {renderMessage(messages[0])}
+
+
+ Try asking about:
+
+
+ {suggestedQuestions.map((question, idx) => (
+ {
+ const messageInput = document.querySelector('input[name="message"]') as HTMLInputElement;
+ if (messageInput) {
+ messageInput.value = question;
+ messageInput.focus();
+ }
+ }}
+ className="block w-full text-left p-2 text-sm text-gray-600 hover:bg-gray-50 rounded border"
+ >
+ {question}
+
+ ))}
+
+
+
+ )}
+
+ {messages.length > 1 && messages.slice(1).map((message) => (
+
+ {renderMessage(message)}
+
+ ))}
+
+ {isLoading && (
+
+ )}
+
+
+
+
+ {/* Input Area */}
+ {
+ e.preventDefault();
+ const formData = new FormData(e.currentTarget);
+ handleSendMessage(formData);
+ }}
+ className="flex space-x-2"
+ >
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* Info Panel */}
+
+
+
+
How it works:
+
+ • Ask questions in natural language
+ • The AI searches through authorized documents in your department
+ • Responses are tailored to your role as a {user.role}
+ • Sources are provided when available
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/demo-login.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/demo-login.tsx
new file mode 100644
index 00000000..a89c6ab1
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/demo-login.tsx
@@ -0,0 +1,241 @@
+import { useState } from 'react';
+import { useFetcher } from 'react-router';
+import {
+ UserCheck,
+ Activity,
+ ShieldCheck,
+ LogIn,
+ Users,
+ FileText,
+ MessageSquare
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { cn } from '@/lib/utils';
+
+interface DemoUser {
+ username: string;
+ password: string;
+ role: string;
+ department: string;
+ description: string;
+ permissions: string[];
+ icon: any;
+ color: string;
+}
+
+const DEMO_USERS: DemoUser[] = [
+ {
+ username: 'admin_wilson',
+ password: 'secure_password',
+ role: 'admin',
+ department: 'administration',
+ description: 'Full system access with administrative privileges',
+ permissions: ['All documents', 'All departments', 'User management', 'System settings', 'RAG chat assistant'],
+ icon: ShieldCheck,
+ color: 'bg-red-500'
+ },
+ {
+ username: 'dr_smith',
+ password: 'secure_password',
+ role: 'doctor',
+ department: 'cardiology',
+ description: 'Cardiology specialist with access to patient records and medical protocols',
+ permissions: ['Cardiology documents', 'Patient records', 'Medical protocols', 'RAG chat assistant'],
+ icon: UserCheck,
+ color: 'bg-blue-500'
+ },
+ {
+ username: 'nurse_johnson',
+ password: 'secure_password',
+ role: 'nurse',
+ department: 'emergency',
+ description: 'Emergency department nurse with access to emergency protocols and procedures',
+ permissions: ['Emergency documents', 'Patient care protocols', 'RAG chat assistant'],
+ icon: Activity,
+ color: 'bg-green-500'
+ }
+];
+
+function getRoleBadgeVariant(role: string) {
+ switch (role) {
+ case 'doctor':
+ return 'doctor' as const;
+ case 'nurse':
+ return 'nurse' as const;
+ case 'admin':
+ return 'admin' as const;
+ default:
+ return 'default' as const;
+ }
+}
+
+export default function DemoLogin() {
+ const fetcher = useFetcher();
+ const [selectedUser, setSelectedUser] = useState(null);
+
+ const handleLogin = (user: DemoUser) => {
+ setSelectedUser(user);
+ const formData = new FormData();
+ formData.append('username', user.username);
+ formData.append('password', user.password);
+
+ fetcher.submit(formData, {
+ method: 'post',
+ action: '/login'
+ });
+ };
+
+ const isLoggingIn = fetcher.state === 'submitting';
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Healthcare Support Portal
+
+
+ Choose a demo user role to explore the system. Each role has different permissions
+ and access to various documents and features.
+
+
+
+ {/* Demo Users Grid */}
+
+ {DEMO_USERS.map((user) => {
+ const IconComponent = user.icon;
+ const isSelected = selectedUser?.username === user.username;
+ const isCurrentUserLoggingIn = isLoggingIn && isSelected;
+
+ return (
+
!isLoggingIn && handleLogin(user)}
+ >
+
+
+ {user.username}
+ {user.description}
+
+
+
+
+
+
+ {user.role}
+
+
+
+
+
+
+ Department: {user.department}
+
+
+
+
+
Permissions:
+
+ {user.permissions.map((permission, idx) => (
+
+
+ {permission}
+
+ ))}
+
+
+
+ {
+ e.stopPropagation();
+ handleLogin(user);
+ }}
+ >
+ {isCurrentUserLoggingIn ? (
+ <>
+
+ Logging in...
+ >
+ ) : (
+ <>
+
+ Login as {user.role}
+ >
+ )}
+
+
+
+ );
+ })}
+
+
+ {/* Features Overview */}
+
+
+
+
+ RAG Chat Assistant Features
+
+
+ Test how the AI assistant responds differently based on user permissions
+
+
+
+
+
+
What you can test:
+
+ • Document access based on user role
+ • Department-specific information retrieval
+ • Permission-based search results
+ • Context-aware responses
+
+
+
+
Try asking:
+
+ • "What are the emergency protocols?"
+ • "Show me cardiology guidelines"
+ • "What documents can I access?"
+ • "Help me with patient care procedures"
+
+
+
+
+
+
+ {/* Info Alert */}
+
+
+
+ Demo Mode: All demo users share the same password: secure_password.
+ This allows you to quickly switch between different user roles to test permissions and see how the RAG system
+ provides different responses based on user access levels.
+
+
+
+
+ );
+}
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/documents.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/documents.tsx
new file mode 100644
index 00000000..2e5daa17
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/documents.tsx
@@ -0,0 +1,479 @@
+import { useState, useEffect } from 'react';
+import {
+ Plus,
+ Search,
+ Filter,
+ FileText,
+ Upload,
+ Eye,
+ Download,
+ AlertCircle,
+ Calendar,
+ User as UserIcon,
+ RefreshCw,
+ Trash2
+} from 'lucide-react';
+import { useLoaderData, Form, useFetcher, Link } from 'react-router';
+import { useForm, getFormProps, getInputProps, getSelectProps } from '@conform-to/react';
+import { parseWithZod } from '@conform-to/zod';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { EmbeddingStatus } from '@/components/embeddings/EmbeddingStatus';
+import { DocumentUpload } from '@/components/documents/DocumentUpload';
+import { formatDateTime, truncateText } from '@/lib/utils';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { handleFormSubmission } from '@/lib/utils/action-utils';
+import { serverApi } from '@/lib/api.server';
+import { documentCreateSchema } from '@/lib/schemas';
+import type { Document, User, Patient } from '@/lib/types';
+import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
+
+interface DocumentsData {
+ user: User;
+ documents: Document[];
+ embeddingStatuses: Record;
+ patients: Patient[];
+}
+
+// Loader function - fetch documents data
+export async function loader({ request }: LoaderFunctionArgs) {
+ try {
+ // Require authentication
+ const user = await requireAuth(request);
+
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ // Load documents, embedding statuses, and patients
+ const [documents, embeddingStatuses, patients] = await Promise.all([
+ serverApi.getDocuments(token),
+ serverApi.getAllEmbeddingStatuses(token),
+ serverApi.getPatients(token)
+ ]);
+
+ return {
+ user,
+ documents,
+ embeddingStatuses,
+ patients
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+// Action function - handle document creation and embedding regeneration
+export async function action({ request }: ActionFunctionArgs) {
+ const user = await requireAuth(request);
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ const formData = await request.formData();
+ const action = formData.get('action');
+
+ // Handle embedding regeneration
+ if (action === 'regenerate') {
+ const documentId = parseInt(formData.get('documentId') as string);
+
+ try {
+ const result = await serverApi.regenerateEmbeddings(documentId, token);
+ return Response.json({
+ success: true,
+ documentId,
+ ...result
+ });
+ } catch (error) {
+ return Response.json({
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to regenerate embeddings'
+ }, { status: 500 });
+ }
+ }
+
+ // Handle document deletion
+ if (action === 'delete') {
+ const documentId = formData.get('documentId') as string;
+
+ // Check if user has permission to delete documents (doctors and admins only)
+ if (!['doctor', 'admin'].includes(user.role)) {
+ return Response.json({
+ success: false,
+ error: 'Access denied. Only doctors and administrators can delete documents.'
+ }, { status: 403 });
+ }
+
+ try {
+ await serverApi.deleteDocument(token, documentId);
+ return Response.json({
+ success: true,
+ message: 'Document deleted successfully'
+ });
+ } catch (error) {
+ return Response.json({
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to delete document'
+ }, { status: 500 });
+ }
+ }
+
+ // Handle document creation
+ return handleFormSubmission(request, documentCreateSchema, async (data) => {
+ // Add user context to document data
+ const documentData = {
+ ...data,
+ department: data.department || user.department, // Use user's department as default
+ };
+
+ await serverApi.createDocument(documentData, token);
+ return Response.json({ success: true, message: 'Document created successfully' });
+ });
+}
+
+export default function Documents() {
+ const { user, documents, embeddingStatuses: initialEmbeddingStatuses, patients } = useLoaderData();
+ const fetcher = useFetcher();
+ const [filteredDocuments, setFilteredDocuments] = useState(documents);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [typeFilter, setTypeFilter] = useState('all');
+ const [departmentFilter, setDepartmentFilter] = useState('all');
+ const [embeddingStatuses, setEmbeddingStatuses] = useState>(initialEmbeddingStatuses);
+
+ useEffect(() => {
+ filterDocuments();
+ }, [documents, searchTerm, typeFilter, departmentFilter]);
+
+ // Handle fetcher response for embedding regeneration
+ useEffect(() => {
+ if (fetcher.data && fetcher.state === 'idle' && fetcher.data.success && fetcher.data.documentId) {
+ setEmbeddingStatuses(prev => ({
+ ...prev,
+ [fetcher.data.documentId]: {
+ has_embeddings: true,
+ embedding_count: fetcher.data.chunks_created || 0
+ }
+ }));
+ }
+ }, [fetcher.data, fetcher.state]);
+
+ const filterDocuments = () => {
+ let filtered = documents;
+
+ // Search filter
+ if (searchTerm) {
+ filtered = filtered.filter(doc =>
+ doc.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ doc.content.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+ }
+
+ // Type filter
+ if (typeFilter !== 'all') {
+ filtered = filtered.filter(doc => doc.document_type === typeFilter);
+ }
+
+ // Department filter
+ if (departmentFilter !== 'all') {
+ filtered = filtered.filter(doc => doc.department === departmentFilter);
+ }
+
+ setFilteredDocuments(filtered);
+ };
+
+ const documentTypes = Array.from(new Set(documents.map(d => d.document_type))).sort();
+ const departments = Array.from(new Set(documents.map(d => d.department))).sort();
+
+ const getDocumentIcon = (type: string) => {
+ switch (type) {
+ case 'protocol':
+ return '📋';
+ case 'policy':
+ return '📜';
+ case 'guideline':
+ return '📖';
+ case 'research':
+ return '🔬';
+ case 'report':
+ return '📊';
+ case 'medical_record':
+ return '📝';
+ default:
+ return '📄';
+ }
+ };
+
+ const handleRegenerateEmbeddings = async (documentId: number) => {
+ // Use fetcher to handle the regeneration
+ const formData = new FormData();
+ formData.append('action', 'regenerate');
+ formData.append('documentId', documentId.toString());
+
+ fetcher.submit(formData, { method: 'post' });
+ };
+
+ const handleDeleteDocument = async (documentId: number, documentTitle: string) => {
+ if (window.confirm(`Are you sure you want to delete "${documentTitle}"? This action cannot be undone.`)) {
+ const formData = new FormData();
+ formData.append('action', 'delete');
+ formData.append('documentId', documentId.toString());
+
+ fetcher.submit(formData, { method: 'post' });
+ }
+ };
+
+
+ return (
+
+ {/* Header */}
+
+
+
+ Documents
+
+
+ Access medical documents, protocols, and policies
+
+
+
+
{
+ const uploadSection = document.querySelector('[data-upload-section]');
+ uploadSection?.scrollIntoView({ behavior: 'smooth' });
+ }}
+ >
+
+ Upload
+
+
+
+
+ New Document
+
+
+
+
+
+ {/* Filters */}
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
setTypeFilter(e.target.value)}
+ className="h-10 px-3 py-2 border border-input bg-background rounded-md text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
+ >
+ All Types
+ {documentTypes.map(type => (
+
+ {type.replace('_', ' ')}
+
+ ))}
+
+
setDepartmentFilter(e.target.value)}
+ className="h-10 px-3 py-2 border border-input bg-background rounded-md text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
+ >
+ All Departments
+ {departments.map(dept => (
+
+ {dept}
+
+ ))}
+
+
+
+ {filteredDocuments.length} documents
+
+
+
+
+
+ {/* Documents Grid */}
+ {filteredDocuments.length === 0 ? (
+
+
+
+ No documents found
+
+ {documents.length === 0
+ ? "Get started by uploading your first document."
+ : "Try adjusting your search or filter criteria."
+ }
+
+ {documents.length === 0 && (
+
+ )}
+
+
+ ) : (
+
+ {filteredDocuments.map((document) => (
+
+
+
+
+
+ {getDocumentIcon(document.document_type)}
+
+
+
+ {document.title}
+
+
+
+ {document.document_type.replace('_', ' ')}
+
+ {document.is_sensitive && (
+
+
+ Sensitive
+
+ )}
+
+
+ handleRegenerateEmbeddings(document.id)}
+ canRegenerate={user?.role === 'admin' || user?.role === 'doctor' || user?.role === 'nurse'}
+ />
+
+
+
+
+
+
+
+
+ {truncateText(document.content, 120)}
+
+
+
+
+
+ Department: {document.department}
+
+
+
+ Created: {formatDateTime(document.created_at)}
+
+ {document.created_by && (
+
+
+ By: {document.created_by.username}
+
+ )}
+
+
+
+
+
+
+ View
+
+
+
+
+
+ {(user?.role === 'doctor' || user?.role === 'admin') && (
+ handleDeleteDocument(document.id, document.title)}
+ className="text-red-600 hover:text-red-700 hover:bg-red-50"
+ >
+
+
+ )}
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Quick Stats */}
+
+
+
+
+
+ {documents.length}
+
+
Total Documents
+
+
+
+
+
+
+
+
+ {documents.filter(d => d.department === user?.department).length}
+
+
My Department
+
+
+
+
+
+
+
+
+ {documents.filter(d => d.is_sensitive).length}
+
+
Sensitive
+
+
+
+
+
+
+
+
+ {documentTypes.length}
+
+
Document Types
+
+
+
+
+
+ {/* Document Upload */}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/documents/$id.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/documents/$id.tsx
new file mode 100644
index 00000000..9310b063
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/documents/$id.tsx
@@ -0,0 +1,391 @@
+import { useLoaderData, Link, useFetcher, redirect } from 'react-router';
+import {
+ ArrowLeft,
+ Download,
+ Edit,
+ Calendar,
+ User as UserIcon,
+ MapPin,
+ FileText,
+ AlertCircle,
+ Shield,
+ Eye,
+ Trash2
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { EmbeddingStatus } from '@/components/embeddings/EmbeddingStatus';
+import { formatDateTime, getDepartmentColor } from '@/lib/utils';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { serverApi } from '@/lib/api.server';
+import type { Document, User } from '@/lib/types';
+import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
+
+interface DocumentDetailData {
+ user: User;
+ document: Document;
+ embeddingStatus: any;
+}
+
+// Loader function - fetch individual document data
+export async function loader({ request, params }: LoaderFunctionArgs) {
+ try {
+ // Require authentication
+ const user = await requireAuth(request);
+
+ const documentId = params.id;
+ if (!documentId || isNaN(Number(documentId))) {
+ throw new Response('Invalid document ID', { status: 400 });
+ }
+
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ // Load document and embedding status
+ const [document, embeddingStatus] = await Promise.all([
+ serverApi.getDocument(parseInt(documentId), token),
+ serverApi.getEmbeddingStatus(parseInt(documentId), token).catch(() => null)
+ ]);
+
+ return {
+ user,
+ document,
+ embeddingStatus
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+// Action function - handle document deletion
+export async function action({ request, params }: ActionFunctionArgs) {
+ const user = await requireAuth(request);
+
+ // Check if user has permission to delete documents (doctors and admins only)
+ if (!['doctor', 'admin'].includes(user.role)) {
+ throw new Response('Access denied. Only doctors and administrators can delete documents.', { status: 403 });
+ }
+
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ const formData = await request.formData();
+ const action = formData.get('action');
+
+ if (action === 'delete') {
+ const documentId = params.id;
+
+ if (!documentId) {
+ throw new Response('Document ID is required', { status: 400 });
+ }
+
+ try {
+ await serverApi.deleteDocument(token, documentId);
+ return redirect('/documents');
+ } catch (error) {
+ throw new Response(
+ error instanceof Error ? error.message : 'Failed to delete document',
+ { status: 500 }
+ );
+ }
+ }
+
+ throw new Response('Invalid action', { status: 400 });
+}
+
+export default function DocumentDetail() {
+ const { user, document: doc, embeddingStatus } = useLoaderData();
+ const fetcher = useFetcher();
+
+ const handleDeleteDocument = () => {
+ if (window.confirm(`Are you sure you want to delete "${doc.title}"? This action cannot be undone.`)) {
+ const formData = new FormData();
+ formData.append('action', 'delete');
+
+ fetcher.submit(formData, { method: 'post' });
+ }
+ };
+
+ const getDocumentIcon = (type: string) => {
+ switch (type) {
+ case 'protocol': return '📋';
+ case 'policy': return '📜';
+ case 'guideline': return '📖';
+ case 'research': return '🔬';
+ case 'report': return '📊';
+ case 'medical_record': return '📝';
+ default: return '📄';
+ }
+ };
+
+ const handleDownload = () => {
+ // Create a blob with the document content
+ const blob = new Blob([doc.content], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+
+ // Create a temporary link and trigger download
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `${doc.title}.txt`;
+ document.body.appendChild(link);
+ link.click();
+
+ // Cleanup
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ Back to Documents
+
+
+
+
+
+ {getDocumentIcon(doc.document_type)}
+
+
+
+ {doc.title}
+
+
+
+ {doc.document_type.replace('_', ' ')}
+
+ {doc.is_sensitive && (
+
+
+ Sensitive
+
+ )}
+
+
+
+
+
+
+
+
+ Download
+
+ {(user?.role === 'admin' || doc.created_by_id === user?.id) && (
+
+
+ Edit Document
+
+ )}
+ {(user?.role === 'doctor' || user?.role === 'admin') && (
+
+
+ {fetcher.state === 'submitting' ? 'Deleting...' : 'Delete Document'}
+
+ )}
+
+
+
+
+ {/* Main Content */}
+
+ {/* Document Content */}
+
+
+ Document Content
+
+ Full content of the document
+
+
+
+
+
+
+
+ {/* AI Embedding Status */}
+ {embeddingStatus && (
+
+
+ AI Processing Status
+
+ Vector embedding status for AI-powered search and chat
+
+
+
+
+
+
+ )}
+
+
+ {/* Sidebar */}
+
+ {/* Document Information */}
+
+
+ Document Details
+
+
+
+
+
Type
+
+ {doc.document_type.replace('_', ' ')}
+
+
+
+
+
Department
+
+
+ {doc.department}
+
+
+
+
+
Created
+
+
+ {formatDateTime(doc.created_at)}
+
+
+
+ {doc.created_by && (
+
+
Created By
+
+
+ {doc.created_by.username}
+
+
+ )}
+
+
+
Sensitivity
+
+ {doc.is_sensitive ? (
+
+
+ Sensitive Document
+
+ ) : (
+
+
+ Standard Access
+
+ )}
+
+
+
+
+
+
+ {/* Associated Patient */}
+ {doc.patient && (
+
+
+ Associated Patient
+
+
+
+
+
+
+
+
+ {doc.patient.name}
+
+
+ MRN: {doc.patient.medical_record_number}
+
+
+ {doc.patient.department}
+
+
+
+
+
+
+ View Patient Details
+
+
+
+
+
+ )}
+
+ {/* Quick Actions */}
+
+
+ Quick Actions
+
+
+
+
+ Download Document
+
+
+ng.
+
+ Chat About Document
+
+
+ {(user?.role === 'admin' || doc.created_by_id === user?.id) && (
+
+
+ Edit Document
+
+ )}
+ {(user?.role === 'doctor' || user?.role === 'admin') && (
+
+
+ {fetcher.state === 'submitting' ? 'Deleting...' : 'Delete Document'}
+
+ )}
+
+
+
+ Back to Documents
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/documents/new.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/documents/new.tsx
new file mode 100644
index 00000000..0c4f20ab
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/documents/new.tsx
@@ -0,0 +1,105 @@
+import { useLoaderData, redirect } from 'react-router';
+import { DocumentForm } from '@/components/documents/DocumentForm';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { handleFormSubmission } from '@/lib/utils/action-utils';
+import { serverApi } from '@/lib/api.server';
+import { documentCreateSchema } from '@/lib/schemas';
+import type { User, Patient } from '@/lib/types';
+import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
+
+interface NewDocumentData {
+ currentUser: User;
+ patients: Patient[];
+}
+
+// Loader function - get current user and available patients
+export async function loader({ request }: LoaderFunctionArgs) {
+ try {
+ // Require authentication
+ const currentUser = await requireAuth(request);
+
+ // Check if user has permission to create documents (doctors, nurses, and admins)
+ if (!['doctor', 'nurse', 'admin'].includes(currentUser.role)) {
+ throw new Response('Access denied. Only doctors, nurses, and administrators can create documents.', { status: 403 });
+ }
+
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ // Get URL search params for pre-filling form
+ const url = new URL(request.url);
+ const patientId = url.searchParams.get('patient_id');
+ const documentType = url.searchParams.get('document_type');
+
+ // Load all patients for association (filtered by authorization on backend)
+ const patients = await serverApi.getPatients(token);
+
+ return {
+ currentUser,
+ patients,
+ defaultValues: {
+ patientId: patientId ? parseInt(patientId) : null,
+ documentType: documentType || null
+ }
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+// Action function - handle document creation
+export async function action({ request }: ActionFunctionArgs) {
+ const currentUser = await requireAuth(request);
+
+ // Check if user has permission to create documents
+ if (!['doctor', 'nurse', 'admin'].includes(currentUser.role)) {
+ throw new Response('Access denied. Only doctors, nurses, and administrators can create documents.', { status: 403 });
+ }
+
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ return handleFormSubmission(request, documentCreateSchema, async (data) => {
+ // Add user context to document data
+ const documentData = {
+ ...data,
+ department: data.department || currentUser.department, // Use user's department as default
+ };
+
+ await serverApi.createDocument(documentData, token);
+ // Redirect to documents list on success
+ return redirect('/documents');
+ });
+}
+
+export default function NewDocument() {
+ const { currentUser, patients } = useLoaderData();
+
+ return (
+
+ {/* Header */}
+
+
+
+ Create New Document
+
+
+ Add a new document to the Healthcare Support Portal with AI-powered features
+
+
+
+
+ {/* Document Form */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/login.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/login.tsx
new file mode 100644
index 00000000..aec9cbec
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/login.tsx
@@ -0,0 +1,238 @@
+import { useState } from 'react';
+import { Form, redirect, useNavigation, Link } from 'react-router';
+import { Activity, Eye, EyeOff, Users, ArrowRight } from 'lucide-react';
+import { useForm, getFormProps, getInputProps } from '@conform-to/react';
+import { parseWithZod } from '@conform-to/zod';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { loginSchema } from '@/lib/schemas';
+import { serverApi } from '@/lib/api.server';
+import { handleFormSubmission, setAuthCookies } from '@/lib/utils/action-utils';
+import { getCurrentUser, json } from '@/lib/utils/loader-utils';
+import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
+
+// Loader function - check if already authenticated
+export async function loader({ request }: LoaderFunctionArgs) {
+ const user = await getCurrentUser(request);
+ if (user) {
+ // Already logged in, redirect to dashboard
+ return redirect('/');
+ }
+ return null;
+}
+
+// Action function - handle login form submission
+export async function action({ request }: ActionFunctionArgs) {
+ return handleFormSubmission(request, loginSchema, async (data) => {
+ try {
+ console.log('[Login] Attempting login for:', data.username);
+
+ // Call auth service to login
+ const authResponse = await serverApi.login(data);
+ console.log('[Login] Auth response received:', authResponse);
+
+ const token = authResponse.access_token;
+ if (!token) {
+ throw new Error('No access token received from auth service');
+ }
+
+ // Get user data
+ const user = await serverApi.getCurrentUser(token);
+ console.log('[Login] User data retrieved:', user);
+
+ // Create redirect response with auth cookies
+ const response = redirect('/');
+ return setAuthCookies(response, token, user);
+ } catch (error: any) {
+ console.error('[Login] Error during login:', error);
+
+ // Return form error
+ const submission = parseWithZod(await request.formData(), { schema: loginSchema });
+ return json({
+ submission: submission.reply({
+ formErrors: [
+ error.response?.data?.detail ||
+ error.message ||
+ 'Invalid username or password'
+ ],
+ }),
+ }, { status: 400 });
+ }
+ });
+}
+
+interface LoginProps {
+ actionData?: {
+ submission?: any;
+ };
+}
+
+export default function Login({ actionData }: LoginProps) {
+ const [showPassword, setShowPassword] = useState(false);
+ const navigation = useNavigation();
+ const isSubmitting = navigation.state === 'submitting';
+
+ const [form, fields] = useForm({
+ lastResult: actionData?.submission,
+ onValidate({ formData }) {
+ return parseWithZod(formData, { schema: loginSchema });
+ },
+ shouldValidate: 'onBlur',
+ shouldRevalidate: 'onInput',
+ });
+
+ return (
+
+
+ {/* Logo and Title */}
+
+
+
+ Healthcare Support Portal
+
+
+ Sign in to your account to continue
+
+
+
+ {/* Login Form */}
+
+
+ Sign In
+
+ Enter your credentials to access the healthcare portal
+
+
+
+
+
+
+
+
+
+
+ Need access?
+
+
+
+
+
+
+
+ Contact your system administrator to request access to the Healthcare Support Portal.
+
+
+
+
+
+
+
+ {/* Demo Login Link */}
+
+
+
+
+
+
+
Quick Demo Access
+
+ Try different user roles and see how permissions affect the RAG chat assistant
+
+
+
+
+ Demo User Switcher
+
+
+
+
+
+
+
+ {/* Demo Credentials */}
+
+
+ Manual Demo Credentials
+
+
Doctor: dr_smith / secure_password
+
Nurse: nurse_johnson / secure_password
+
Admin: admin_wilson / secure_password
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/logout.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/logout.tsx
new file mode 100644
index 00000000..26357333
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/logout.tsx
@@ -0,0 +1,14 @@
+import { redirect } from 'react-router';
+import { clearAuthCookies } from '@/lib/utils/action-utils';
+import type { ActionFunctionArgs } from 'react-router';
+
+// Action function - handle logout
+export async function action({ request }: ActionFunctionArgs) {
+ // Clear auth cookies and redirect to login
+ return clearAuthCookies();
+}
+
+// This route should never render - it's action-only
+export default function Logout() {
+ return null;
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/patients/$id.edit.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/patients/$id.edit.tsx
new file mode 100644
index 00000000..0b52866c
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/patients/$id.edit.tsx
@@ -0,0 +1,109 @@
+import { useLoaderData, redirect } from 'react-router';
+import { PatientForm } from '@/components/patients/PatientForm';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { handleFormSubmission } from '@/lib/utils/action-utils';
+import { serverApi } from '@/lib/api.server';
+import { patientUpdateSchema } from '@/lib/schemas';
+import type { User, Patient } from '@/lib/types';
+import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
+
+interface EditPatientData {
+ currentUser: User;
+ patient: Patient;
+ doctors: User[];
+}
+
+// Loader function - get patient data and available doctors
+export async function loader({ request, params }: LoaderFunctionArgs) {
+ try {
+ // Require authentication
+ const currentUser = await requireAuth(request);
+
+ // Check if user has permission to edit patients (doctors and admins only)
+ if (!['doctor', 'nurse', 'admin'].includes(currentUser.role)) {
+ throw new Response('Access denied. Only doctors, nurses, and administrators can edit patients.', { status: 403 });
+ }
+
+ // Get patient ID from params
+ const patientId = parseInt(params.id as string);
+ if (isNaN(patientId)) {
+ throw new Response('Invalid patient ID', { status: 400 });
+ }
+
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ // Load patient data and all users to get doctors for assignment
+ const [patient, users] = await Promise.all([
+ serverApi.getPatient(patientId, token),
+ serverApi.getUsers(token)
+ ]);
+
+ const doctors = users.filter((user: User) => user.role === 'doctor' && user.is_active);
+
+ return {
+ currentUser,
+ patient,
+ doctors
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+// Action function - handle patient update
+export async function action({ request, params }: ActionFunctionArgs) {
+ const currentUser = await requireAuth(request);
+
+ // Check if user has permission to edit patients
+ if (!['doctor', 'admin'].includes(currentUser.role)) {
+ throw new Response('Access denied. Only doctors and administrators can edit patients.', { status: 403 });
+ }
+
+ // Get patient ID from params
+ const patientId = parseInt(params.id as string);
+ if (isNaN(patientId)) {
+ throw new Response('Invalid patient ID', { status: 400 });
+ }
+
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ return handleFormSubmission(request, patientUpdateSchema, async (data) => {
+ await serverApi.updatePatient(patientId, data, token);
+ // Redirect to patient detail page on success
+ return redirect(`/patients/${patientId}`);
+ });
+}
+
+export default function EditPatient() {
+ const { currentUser, patient, doctors } = useLoaderData();
+
+ return (
+
+ {/* Header */}
+
+
+
+ Edit Patient: {patient.name}
+
+
+ Update patient information and assignments
+
+
+
+
+ {/* Patient Form */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/patients/index.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/patients/index.tsx
new file mode 100644
index 00000000..680f5255
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/patients/index.tsx
@@ -0,0 +1,285 @@
+import { useState, useEffect } from 'react';
+import { Link, useLoaderData, redirect } from 'react-router';
+import { Plus, Search, Filter, Users, Calendar, Phone, Mail } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { formatDate, getDepartmentColor } from '@/lib/utils';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { handleFormSubmission } from '@/lib/utils/action-utils';
+import { serverApi } from '@/lib/api.server';
+import { patientCreateSchema } from '@/lib/schemas';
+import type { Patient, User } from '@/lib/types';
+import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
+
+interface PatientsData {
+ user: User;
+ patients: Patient[];
+}
+
+// Loader function - fetch patients data
+export async function loader({ request }: LoaderFunctionArgs) {
+ try {
+ // Require authentication
+ const user = await requireAuth(request);
+
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ // Load patients
+ const patients = await serverApi.getPatients(token);
+
+ return {
+ user,
+ patients
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+// Action function - handle patient creation
+export async function action({ request }: ActionFunctionArgs) {
+ const user = await requireAuth(request);
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ return handleFormSubmission(request, patientCreateSchema, async (data) => {
+ // Add user context to patient data
+ const patientData = {
+ ...data,
+ department: data.department || user.department, // Use user's department as default
+ };
+
+ await serverApi.createPatient(patientData, token);
+ return redirect('/patients');
+ });
+}
+
+export default function PatientsIndex() {
+ const { user, patients } = useLoaderData();
+ const [filteredPatients, setFilteredPatients] = useState(patients);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [departmentFilter, setDepartmentFilter] = useState('all');
+
+ useEffect(() => {
+ filterPatients();
+ }, [patients, searchTerm, departmentFilter]);
+
+ const filterPatients = () => {
+ let filtered = patients;
+
+ // Search filter
+ if (searchTerm) {
+ filtered = filtered.filter(patient =>
+ patient.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ patient.medical_record_number.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+ }
+
+ // Department filter
+ if (departmentFilter !== 'all') {
+ filtered = filtered.filter(patient => patient.department === departmentFilter);
+ }
+
+ setFilteredPatients(filtered);
+ };
+
+ const departments = Array.from(new Set(patients.map(p => p.department))).sort();
+
+
+ return (
+
+ {/* Header */}
+
+
+
+ Patients
+
+
+ Manage patient records and assignments
+
+
+
+
+
+
+ Add Patient
+
+
+
+
+
+ {/* Filters */}
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
setDepartmentFilter(e.target.value)}
+ className="h-10 px-3 py-2 border border-input bg-background rounded-md text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
+ >
+ All Departments
+ {departments.map(dept => (
+
+ {dept}
+
+ ))}
+
+
+
+ {filteredPatients.length} patients
+
+
+
+
+
+ {/* Patients Grid */}
+ {filteredPatients.length === 0 ? (
+
+
+
+ No patients found
+
+ {patients.length === 0
+ ? "Get started by adding your first patient."
+ : "Try adjusting your search or filter criteria."
+ }
+
+ {patients.length === 0 && (
+
+
+
+
+ Add Patient
+
+
+
+ )}
+
+
+ ) : (
+
+ {filteredPatients.map((patient) => (
+
+
+
+
+
+
+
+
+ {patient.name}
+
+ MRN: {patient.medical_record_number}
+
+
+
+
+ {patient.is_active ? 'Active' : 'Inactive'}
+
+
+
+
+
+
+
+ {patient.date_of_birth
+ ? `DOB: ${formatDate(patient.date_of_birth)}`
+ : 'DOB: Not specified'
+ }
+
+
+
+ Department: {patient.department}
+
+ {patient.assigned_doctor && (
+
+
+ Dr. {patient.assigned_doctor.username}
+
+ )}
+
+
+
+
+ View Details
+
+
+
+
+ Edit
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Quick Stats */}
+
+
+
+
+
+ {patients.filter(p => p.is_active).length}
+
+
Active Patients
+
+
+
+
+
+
+
+ {patients.filter(p => p.assigned_doctor_id === user?.id).length}
+
+
My Patients
+
+
+
+
+
+
+
+ {patients.filter(p => p.department === user?.department).length}
+
+
Department
+
+
+
+
+
+
+
+ {departments.length}
+
+
Departments
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/patients/new.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/patients/new.tsx
new file mode 100644
index 00000000..b7b340a3
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/patients/new.tsx
@@ -0,0 +1,103 @@
+import { useLoaderData, redirect } from 'react-router';
+import { PatientForm } from '@/components/patients/PatientForm';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { handleFormSubmission } from '@/lib/utils/action-utils';
+import { serverApi } from '@/lib/api.server';
+import { patientCreateSchema } from '@/lib/schemas';
+import type { User } from '@/lib/types';
+import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
+
+interface NewPatientData {
+ currentUser: User;
+ doctors: User[];
+}
+
+// Loader function - get current user and available doctors
+export async function loader({ request }: LoaderFunctionArgs) {
+ try {
+ // Require authentication
+ const currentUser = await requireAuth(request);
+
+ // Check if user has permission to create patients (doctors, nurses, and admins)
+ if (!['doctor', 'nurse', 'admin'].includes(currentUser.role)) {
+ throw new Response('Access denied. Only doctors, nurses, and administrators can create patients.', { status: 403 });
+ }
+
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ // Load all users to get doctors for assignment
+ let doctors: User[] = [];
+ try {
+ const users = await serverApi.getUsers(token);
+ doctors = users.filter((user: User) => user.role === 'doctor' && user.is_active);
+ } catch (error) {
+ console.warn('Failed to load doctors list:', error);
+ // Continue without doctors list - form will still work
+ }
+
+ return {
+ currentUser,
+ doctors
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+// Action function - handle patient creation
+export async function action({ request }: ActionFunctionArgs) {
+ const currentUser = await requireAuth(request);
+
+ // Check if user has permission to create patients
+ if (!['doctor', 'nurse', 'admin'].includes(currentUser.role)) {
+ throw new Response('Access denied. Only doctors, nurses, and administrators can create patients.', { status: 403 });
+ }
+
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ return handleFormSubmission(request, patientCreateSchema, async (data) => {
+ // Add user context to patient data
+ const patientData = {
+ ...data,
+ department: data.department || currentUser.department, // Use user's department as default
+ };
+
+ await serverApi.createPatient(token, patientData);
+ // Redirect to patients list on success
+ return redirect('/patients');
+ });
+}
+
+export default function NewPatient() {
+ const { currentUser, doctors } = useLoaderData();
+
+ return (
+
+ {/* Header */}
+
+
+
+ Add New Patient
+
+
+ Create a new patient record for the Healthcare Support Portal
+
+
+
+
+ {/* Patient Form */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/patients/patient.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/patients/patient.tsx
new file mode 100644
index 00000000..408507f1
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/patients/patient.tsx
@@ -0,0 +1,356 @@
+import { useLoaderData, Link, redirect } from 'react-router';
+import {
+ ArrowLeft,
+ Edit,
+ Calendar,
+ User as UserIcon,
+ MapPin,
+ Phone,
+ Mail,
+ FileText,
+ Activity
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { formatDate, formatDateTime, getDepartmentColor } from '@/lib/utils';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { handleFormSubmission } from '@/lib/utils/action-utils';
+import { serverApi } from '@/lib/api.server';
+import { patientUpdateSchema } from '@/lib/schemas';
+import type { Patient, User } from '@/lib/types';
+import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
+
+interface PatientData {
+ user: User;
+ patient: Patient;
+}
+
+// Loader function - fetch individual patient data
+export async function loader({ request, params }: LoaderFunctionArgs) {
+ try {
+ // Require authentication
+ const user = await requireAuth(request);
+
+ const patientId = params.id;
+ if (!patientId || isNaN(Number(patientId))) {
+ throw new Response('Invalid patient ID', { status: 400 });
+ }
+
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ // Load patient
+ const patient = await serverApi.getPatient(parseInt(patientId), token);
+
+ return {
+ user,
+ patient
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+// Action function - handle patient updates
+export async function action({ request, params }: ActionFunctionArgs) {
+ const user = await requireAuth(request);
+ const patientId = params.id;
+
+ if (!patientId || isNaN(Number(patientId))) {
+ throw new Response('Invalid patient ID', { status: 400 });
+ }
+
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ return handleFormSubmission(request, patientUpdateSchema, async (data) => {
+ await serverApi.updatePatient(parseInt(patientId), data, token);
+ return redirect(`/patients/${patientId}`);
+ });
+}
+
+export default function PatientDetail() {
+ const { user, patient } = useLoaderData();
+
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ Back to Patients
+
+
+
+
+ {patient.name}
+
+
+ Medical Record Number: {patient.medical_record_number}
+
+
+
+
+
+ {patient.is_active ? 'Active' : 'Inactive'}
+
+
+
+
+ Edit Patient
+
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Patient Information */}
+
+
+ Patient Information
+
+ Basic patient details and demographics
+
+
+
+
+
+
Full Name
+ {patient.name}
+
+
+
+
Medical Record Number
+ {patient.medical_record_number}
+
+
+
+
Date of Birth
+
+
+ {patient.date_of_birth
+ ? formatDate(patient.date_of_birth)
+ : 'Not specified'
+ }
+
+
+
+
+
Department
+
+
+ {patient.department}
+
+
+
+
+
Status
+
+
+ {patient.is_active ? 'Active' : 'Inactive'}
+
+
+
+
+
+
Created
+
+ {formatDateTime(patient.created_at)}
+
+
+
+
+
+
+ {/* Medical History */}
+
+
+ Medical History
+
+ Patient's medical history and records
+
+
+
+
+
+
+ No medical history available
+
+
+ Medical history and records will appear here once added.
+
+
+
+ Add Medical Record
+
+
+
+
+
+
+ {/* Recent Activity */}
+
+
+ Recent Activity
+
+ Recent updates and changes to this patient record
+
+
+
+
+
+
+
+
+ Patient record created
+
+
+ {formatDateTime(patient.created_at)}
+
+
+
+
+ {patient.assigned_doctor && (
+
+
+
+
+ Assigned to Dr. {patient.assigned_doctor.username}
+
+
+ {patient.assigned_doctor.department} department
+
+
+
+ )}
+
+
+
+
+
+ {/* Sidebar */}
+
+ {/* Assigned Doctor */}
+ {patient.assigned_doctor && (
+
+
+ Assigned Doctor
+
+
+
+
+
+
+
+
+ Dr. {patient.assigned_doctor.username}
+
+
+ {patient.assigned_doctor.department}
+
+
+ Doctor
+
+
+
+
+
+
+ Send Message
+
+
+
+ Contact
+
+
+
+
+ )}
+
+ {/* Quick Actions */}
+
+
+ Quick Actions
+
+
+
+
+
+ Edit Patient Info
+
+
+
+
+
+ Add Medical Record
+
+
+
+
+ Schedule Appointment
+
+
+
+
+ AI Assistant
+
+
+
+
+
+ {/* Patient Stats */}
+
+
+ Patient Summary
+
+
+
+
+ Status
+
+ {patient.is_active ? 'Active' : 'Inactive'}
+
+
+
+ Department
+ {patient.department}
+
+
+ Records
+ 0
+
+
+ Last Visit
+ N/A
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/settings.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/settings.tsx
new file mode 100644
index 00000000..7dced2d8
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/settings.tsx
@@ -0,0 +1,336 @@
+import { useLoaderData } from 'react-router';
+import {
+ Settings as SettingsIcon,
+ Users,
+ Shield,
+ Database,
+ Bell,
+ Key,
+ Globe,
+ Palette,
+ Activity,
+ UserCheck,
+ ShieldCheck,
+ AlertCircle,
+ CheckCircle
+} from 'lucide-react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { serverApi } from '@/lib/api.server';
+import type { User } from '@/lib/types';
+import type { LoaderFunctionArgs } from 'react-router';
+
+interface SystemStats {
+ totalUsers: number;
+ activeUsers: number;
+ totalPatients: number;
+ totalDocuments: number;
+ systemUptime: string;
+}
+
+interface SettingsData {
+ user: User;
+ stats: SystemStats;
+ departments: string[];
+ roles: string[];
+}
+
+// Loader function - require admin authentication
+export async function loader({ request }: LoaderFunctionArgs) {
+ try {
+ // Require authentication and admin role
+ const user = await requireAuth(request);
+
+ // Check if user is admin
+ if (user.role !== 'admin') {
+ throw new Response('Access denied. Admin privileges required.', { status: 403 });
+ }
+
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ // Load system data in parallel
+ const [patients, documents] = await Promise.all([
+ serverApi.getPatients(token).catch(() => []),
+ serverApi.getDocuments(token).catch(() => [])
+ ]);
+
+ // Mock system stats (in a real app, these would come from dedicated admin endpoints)
+ const stats: SystemStats = {
+ totalUsers: 25, // Mock - would come from user service
+ activeUsers: 18, // Mock - would come from user service
+ totalPatients: patients.length,
+ totalDocuments: documents.length,
+ systemUptime: '7 days, 14 hours' // Mock - would come from system monitoring
+ };
+
+ // System configuration options
+ const departments = ['cardiology', 'neurology', 'pediatrics', 'oncology', 'emergency', 'endocrinology', 'obgyn', 'general'];
+ const roles = ['doctor', 'nurse', 'admin'];
+
+ return {
+ user,
+ stats,
+ departments,
+ roles
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+function getUserDisplayName(user: User | null): string {
+ if (!user) return 'Unknown User';
+ return user.username || 'Unknown User';
+}
+
+export default function Settings() {
+ const { user, stats, departments, roles } = useLoaderData();
+
+ const settingsSections = [
+ {
+ id: 'users',
+ title: 'User Management',
+ description: 'Manage user accounts, roles, and permissions',
+ icon: Users,
+ color: 'bg-blue-500',
+ items: [
+ { label: 'Total Users', value: stats.totalUsers },
+ { label: 'Active Users', value: stats.activeUsers },
+ { label: 'Pending Registrations', value: '3' }
+ ]
+ },
+ {
+ id: 'system',
+ title: 'System Configuration',
+ description: 'Configure departments, roles, and system settings',
+ icon: SettingsIcon,
+ color: 'bg-green-500',
+ items: [
+ { label: 'Departments', value: departments.length },
+ { label: 'User Roles', value: roles.length },
+ { label: 'System Uptime', value: stats.systemUptime }
+ ]
+ },
+ {
+ id: 'security',
+ title: 'Security & Access',
+ description: 'Manage authentication and authorization policies',
+ icon: Shield,
+ color: 'bg-red-500',
+ items: [
+ { label: 'Active Sessions', value: '24' },
+ { label: 'Failed Login Attempts (24h)', value: '2' },
+ { label: 'Password Policy', value: 'Strong' }
+ ]
+ },
+ {
+ id: 'data',
+ title: 'Data Management',
+ description: 'Manage patient data, documents, and backups',
+ icon: Database,
+ color: 'bg-purple-500',
+ items: [
+ { label: 'Total Patients', value: stats.totalPatients },
+ { label: 'Total Documents', value: stats.totalDocuments },
+ { label: 'Last Backup', value: '2 hours ago' }
+ ]
+ }
+ ];
+
+ const systemServices = [
+ { name: 'Auth Service', status: 'healthy', port: '8001' },
+ { name: 'Patient Service', status: 'healthy', port: '8002' },
+ { name: 'RAG Service', status: 'healthy', port: '8003' },
+ { name: 'Database', status: 'healthy', port: '5432' },
+ { name: 'Oso Dev Server', status: 'healthy', port: '8080' }
+ ];
+
+ return (
+
+ {/* Header */}
+
+
+
+ System Settings
+
+
+ Manage system configuration and administrative settings
+
+
+
+
+
+ {getUserDisplayName(user)} • Administrator
+
+
+
+
+ {/* Settings Sections */}
+
+ {settingsSections.map((section) => (
+
+
+
+
+
+
+
+ {section.title}
+ {section.description}
+
+
+
+
+
+ {section.items.map((item, index) => (
+
+ {item.label}
+ {item.value}
+
+ ))}
+
+
+
+ Configure {section.title}
+
+
+
+
+ ))}
+
+
+ {/* System Services Status */}
+
+
+ Service Status
+
+ Current status of healthcare portal microservices
+
+
+
+
+ {systemServices.map((service) => (
+
+
+
+ {service.status === 'healthy' ? (
+
+ ) : (
+
+ )}
+
+ {service.name}
+
+
+
+
+ :{service.port}
+
+
+ ))}
+
+
+
+
+ {/* Configuration Options */}
+
+ {/* Departments Configuration */}
+
+
+ Departments
+
+ Available departments in the healthcare system
+
+
+
+
+ {departments.map((dept) => (
+
+ {dept}
+
+ Active
+
+
+ ))}
+
+
+
+ Manage Departments
+
+
+
+
+
+ {/* Roles Configuration */}
+
+
+ User Roles
+
+ Available user roles and their permissions
+
+
+
+
+ {roles.map((role) => (
+
+
+ {role === 'doctor' &&
}
+ {role === 'nurse' &&
}
+ {role === 'admin' &&
}
+
{role}
+
+
+ {role}
+
+
+ ))}
+
+
+
+ Configure Permissions
+
+
+
+
+
+
+ {/* Quick Actions */}
+
+
+ Quick Actions
+
+ Common administrative tasks and system operations
+
+
+
+
+
+
+ Add User
+
+
+
+ Backup Data
+
+
+
+ System Alerts
+
+
+
+ View Logs
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/users/$id.edit.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/users/$id.edit.tsx
new file mode 100644
index 00000000..4d306920
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/users/$id.edit.tsx
@@ -0,0 +1,106 @@
+import { useLoaderData, redirect } from 'react-router';
+import { UserForm } from '@/components/users/UserForm';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { handleFormSubmission } from '@/lib/utils/action-utils';
+import { serverApi } from '@/lib/api.server';
+import { z } from 'zod';
+import type { User } from '@/lib/types';
+import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
+
+const updateUserSchema = z.object({
+ username: z.string().min(3, 'Username must be at least 3 characters'),
+ email: z.string().email('Invalid email address'),
+ password: z.string().optional(),
+ role: z.enum(['doctor', 'nurse', 'admin']),
+ department: z.enum(['cardiology', 'neurology', 'pediatrics', 'oncology', 'emergency', 'endocrinology', 'obgyn', 'general']),
+});
+
+interface EditUserData {
+ currentUser: User;
+ user: User;
+}
+
+// Loader function - load user to edit
+export async function loader({ request, params }: LoaderFunctionArgs) {
+ try {
+ // Require authentication and admin role
+ const currentUser = await requireAuth(request);
+
+ // Check if user is admin
+ if (currentUser.role !== 'admin') {
+ throw new Response('Access denied. Admin privileges required.', { status: 403 });
+ }
+
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ // Load the user to edit
+ const userId = params.id as string;
+ const user = await serverApi.getUser(userId, token);
+
+ return {
+ currentUser,
+ user
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+// Action function - handle user update
+export async function action({ request, params }: ActionFunctionArgs) {
+ const currentUser = await requireAuth(request);
+
+ // Check if user is admin
+ if (currentUser.role !== 'admin') {
+ throw new Response('Access denied. Admin privileges required.', { status: 403 });
+ }
+
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ const userId = parseInt(params.id as string);
+
+ return handleFormSubmission(request, updateUserSchema, async (data) => {
+ // Don't send empty password
+ const updateData = { ...data };
+ if (!updateData.password) {
+ delete updateData.password;
+ }
+
+ await serverApi.updateUser(userId, updateData, token);
+ return redirect('/users');
+ });
+}
+
+export default function EditUser() {
+ const { currentUser, user } = useLoaderData();
+
+ return (
+
+ {/* Header */}
+
+
+
+ Edit User: {user.username}
+
+
+ Update user account information and permissions
+
+
+
+
+ {/* User Form */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/users/index.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/users/index.tsx
new file mode 100644
index 00000000..fb41c1a1
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/users/index.tsx
@@ -0,0 +1,353 @@
+import { useState, useEffect } from 'react';
+import { Link, useLoaderData } from 'react-router';
+import {
+ Plus,
+ Search,
+ Filter,
+ Users,
+ Edit,
+ Mail,
+ Shield,
+ Building,
+ UserCheck,
+ MoreVertical
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { formatDateTime, getDepartmentColor } from '@/lib/utils';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { serverApi } from '@/lib/api.server';
+import type { User } from '@/lib/types';
+import type { LoaderFunctionArgs } from 'react-router';
+
+interface UsersData {
+ currentUser: User;
+ users: User[];
+}
+
+// Loader function - fetch users data
+export async function loader({ request }: LoaderFunctionArgs) {
+ try {
+ // Require authentication and admin role
+ const currentUser = await requireAuth(request);
+
+ // Check if user is admin
+ if (currentUser.role !== 'admin') {
+ throw new Response('Access denied. Admin privileges required.', { status: 403 });
+ }
+
+ // Get auth token from cookies
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ // Load users
+ const users = await serverApi.getUsers(token);
+
+ return {
+ currentUser,
+ users
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+export default function UsersIndex() {
+ const { currentUser, users } = useLoaderData();
+ const [filteredUsers, setFilteredUsers] = useState(users);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [roleFilter, setRoleFilter] = useState('all');
+ const [departmentFilter, setDepartmentFilter] = useState('all');
+
+ useEffect(() => {
+ filterUsers();
+ }, [users, searchTerm, roleFilter, departmentFilter]);
+
+ const filterUsers = () => {
+ let filtered = users;
+
+ // Search filter
+ if (searchTerm) {
+ filtered = filtered.filter(user =>
+ user.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ user.email.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+ }
+
+ // Role filter
+ if (roleFilter !== 'all') {
+ filtered = filtered.filter(user => user.role === roleFilter);
+ }
+
+ // Department filter
+ if (departmentFilter !== 'all') {
+ filtered = filtered.filter(user => user.department === departmentFilter);
+ }
+
+ setFilteredUsers(filtered);
+ };
+
+ const departments = Array.from(new Set(users.map(u => u.department))).sort();
+ const roles = Array.from(new Set(users.map(u => u.role))).sort();
+
+ const getRoleBadgeVariant = (role: string) => {
+ switch(role) {
+ case 'admin': return 'destructive';
+ case 'doctor': return 'default';
+ case 'nurse': return 'secondary';
+ default: return 'outline';
+ }
+ };
+
+ const getRoleIcon = (role: string) => {
+ switch(role) {
+ case 'admin': return ;
+ case 'doctor': return ;
+ default: return ;
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+ User Management
+
+
+ Manage user accounts and permissions
+
+
+
+
+
+ {/* Filters */}
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
setRoleFilter(e.target.value)}
+ className="h-10 px-3 py-2 border border-input bg-background rounded-md text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
+ >
+ All Roles
+ {roles.map(role => (
+
+ {role.charAt(0).toUpperCase() + role.slice(1)}
+
+ ))}
+
+
setDepartmentFilter(e.target.value)}
+ className="h-10 px-3 py-2 border border-input bg-background rounded-md text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
+ >
+ All Departments
+ {departments.map(dept => (
+
+ {dept.charAt(0).toUpperCase() + dept.slice(1)}
+
+ ))}
+
+
+
+ {filteredUsers.length} users
+
+
+
+
+
+ {/* Users Table */}
+
+
+ Users
+
+ A list of all users in the Healthcare Support Portal
+
+
+
+ {filteredUsers.length === 0 ? (
+
+
+
No users found
+
+ {users.length === 0
+ ? "Get started by adding your first user."
+ : "Try adjusting your search or filter criteria."
+ }
+
+ {users.length === 0 && (
+
+ )}
+
+ ) : (
+
+
+
+ User
+ Role
+ Department
+ Status
+ Created
+ Actions
+
+
+
+ {filteredUsers.map((user) => (
+
+
+
+ {user.username}
+
+
+ {user.email}
+
+
+
+
+
+
+ {getRoleIcon(user.role)}
+ {user.role}
+
+
+
+
+
+
+ {user.department}
+
+
+
+
+ {user.is_active ? 'Active' : 'Inactive'}
+
+
+
+ {formatDateTime(user.created_at)}
+
+
+
+
+
+
+
+
+
+ Actions
+
+
+
+
+ Edit User
+
+
+
+ Deactivate User
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Quick Stats */}
+
+
+
+
+
+ {users.filter(u => u.is_active).length}
+
+
Active Users
+
+
+
+
+
+
+
+ {users.filter(u => u.role === 'admin').length}
+
+
Administrators
+
+
+
+
+
+
+
+ {users.filter(u => u.role === 'doctor').length}
+
+
Doctors
+
+
+
+
+
+
+
+ {users.filter(u => u.role === 'nurse').length}
+
+
Nurses
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/routes/users/new.tsx b/python/rag/healthcare-support-portal/frontend/app/routes/users/new.tsx
new file mode 100644
index 00000000..edfc9d69
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/routes/users/new.tsx
@@ -0,0 +1,84 @@
+import { useLoaderData, redirect } from 'react-router';
+import { UserForm } from '@/components/users/UserForm';
+import { requireAuth, handleApiError } from '@/lib/utils/loader-utils';
+import { handleFormSubmission } from '@/lib/utils/action-utils';
+import { serverApi } from '@/lib/api.server';
+import { z } from 'zod';
+import type { User } from '@/lib/types';
+import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
+
+const createUserSchema = z.object({
+ username: z.string().min(3, 'Username must be at least 3 characters'),
+ email: z.string().email('Invalid email address'),
+ password: z.string().min(8, 'Password must be at least 8 characters'),
+ role: z.enum(['doctor', 'nurse', 'admin']),
+ department: z.enum(['cardiology', 'neurology', 'pediatrics', 'oncology', 'emergency', 'endocrinology', 'obgyn', 'general']),
+});
+
+interface NewUserData {
+ currentUser: User;
+}
+
+// Loader function - ensure admin access
+export async function loader({ request }: LoaderFunctionArgs) {
+ try {
+ // Require authentication and admin role
+ const currentUser = await requireAuth(request);
+
+ // Check if user is admin
+ if (currentUser.role !== 'admin') {
+ throw new Response('Access denied. Admin privileges required.', { status: 403 });
+ }
+
+ return {
+ currentUser
+ };
+ } catch (error) {
+ throw handleApiError(error);
+ }
+}
+
+// Action function - handle user creation
+export async function action({ request }: ActionFunctionArgs) {
+ const currentUser = await requireAuth(request);
+
+ // Check if user is admin
+ if (currentUser.role !== 'admin') {
+ throw new Response('Access denied. Admin privileges required.', { status: 403 });
+ }
+
+ const cookieHeader = request.headers.get('Cookie');
+ const token = cookieHeader?.match(/authToken=([^;]+)/)?.[1];
+
+ if (!token) {
+ throw new Response('Authentication required', { status: 401 });
+ }
+
+ return handleFormSubmission(request, createUserSchema, async (data) => {
+ await serverApi.createUser(data, token);
+ return redirect('/users');
+ });
+}
+
+export default function NewUser() {
+ const { currentUser } = useLoaderData();
+
+ return (
+
+ {/* Header */}
+
+
+
+ Add New User
+
+
+ Create a new user account for the Healthcare Support Portal
+
+
+
+
+ {/* User Form */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/app/styles/globals.css b/python/rag/healthcare-support-portal/frontend/app/styles/globals.css
new file mode 100644
index 00000000..79522383
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/app/styles/globals.css
@@ -0,0 +1,345 @@
+@import "tailwindcss";
+@import "highlight.js/styles/github-dark.css";
+
+/* Custom healthcare theme configuration using TailwindCSS v4 @theme directive */
+@theme {
+ --color-primary: #3b82f6;
+ --color-secondary: #64748b;
+ --color-accent: #06b6d4;
+ --color-healthcare-blue: #1e40af;
+ --color-healthcare-green: #059669;
+ --color-danger: #dc2626;
+ --color-success: #16a34a;
+ --color-warning: #d97706;
+
+ /* Healthcare-specific spacing */
+ --spacing-card: 1.5rem;
+ --spacing-section: 2rem;
+
+ /* Healthcare-specific border radius */
+ --radius-card: 0.5rem;
+ --radius-button: 0.375rem;
+
+ /* Healthcare-specific shadows */
+ --shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
+ --shadow-elevated: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+}
+
+/* Base styles */
+* {
+ border-color: hsl(var(--border));
+}
+
+body {
+ color: hsl(var(--foreground));
+ background: hsl(var(--background));
+ font-feature-settings: "rlig" 1, "calt" 1;
+}
+
+/* Healthcare-specific component styles */
+.healthcare-card {
+ @apply bg-white border border-gray-200 rounded-lg shadow-sm p-6;
+}
+
+.healthcare-card-elevated {
+ @apply bg-white border border-gray-200 rounded-lg shadow-md p-6;
+}
+
+/* Role-based badge styles */
+.role-badge-doctor {
+ @apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-blue-100 text-blue-800;
+}
+
+.role-badge-nurse {
+ @apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-green-100 text-green-800;
+}
+
+.role-badge-admin {
+ @apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-purple-100 text-purple-800;
+}
+
+/* Department indicator styles */
+.dept-cardiology {
+ @apply border-l-4 border-red-500;
+}
+
+.dept-neurology {
+ @apply border-l-4 border-purple-500;
+}
+
+.dept-pediatrics {
+ @apply border-l-4 border-yellow-500;
+}
+
+.dept-oncology {
+ @apply border-l-4 border-pink-500;
+}
+
+.dept-emergency {
+ @apply border-l-4 border-orange-500;
+}
+
+.dept-endocrinology {
+ @apply border-l-4 border-teal-500;
+}
+
+.dept-obgyn {
+ @apply border-l-4 border-rose-500;
+}
+
+.dept-general {
+ @apply border-l-4 border-blue-500;
+}
+
+/* Status indicator styles */
+.status-active {
+ @apply text-green-600 bg-green-50 border border-green-200;
+}
+
+.status-inactive {
+ @apply text-gray-600 bg-gray-50 border border-gray-200;
+}
+
+.status-pending {
+ @apply text-yellow-600 bg-yellow-50 border border-yellow-200;
+}
+
+.status-urgent {
+ @apply text-red-600 bg-red-50 border border-red-200;
+}
+
+/* Chat interface styles */
+.chat-bubble-user {
+ @apply bg-blue-600 text-white rounded-lg rounded-br-sm px-4 py-2 max-w-xs ml-auto;
+}
+
+.chat-bubble-assistant {
+ @apply bg-gray-100 text-gray-900 rounded-lg rounded-bl-sm px-4 py-2 max-w-xs mr-auto;
+}
+
+.chat-bubble-system {
+ @apply bg-green-50 text-green-800 rounded-lg px-4 py-2 max-w-md mx-auto text-center text-sm;
+}
+
+/* Markdown content styles for assistant responses */
+.markdown-content {
+ @apply text-sm leading-relaxed;
+}
+
+.markdown-content h1 {
+ @apply text-lg font-bold text-healthcare-blue mb-2 mt-3 first:mt-0;
+}
+
+.markdown-content h2 {
+ @apply text-base font-bold text-healthcare-blue mb-2 mt-3 first:mt-0;
+}
+
+.markdown-content h3 {
+ @apply text-sm font-semibold text-gray-800 mb-1 mt-2 first:mt-0;
+}
+
+.markdown-content h4,
+.markdown-content h5,
+.markdown-content h6 {
+ @apply text-sm font-medium text-gray-700 mb-1 mt-2 first:mt-0;
+}
+
+.markdown-content p {
+ @apply mb-2 last:mb-0;
+}
+
+.markdown-content ul,
+.markdown-content ol {
+ @apply mb-2 pl-4 space-y-1;
+}
+
+.markdown-content ul {
+ @apply list-disc;
+}
+
+.markdown-content ol {
+ @apply list-decimal;
+}
+
+.markdown-content li {
+ @apply text-sm;
+}
+
+.markdown-content blockquote {
+ @apply border-l-4 border-healthcare-blue pl-3 py-1 my-2 bg-blue-50 text-gray-700 italic;
+}
+
+.markdown-content code {
+ @apply bg-gray-200 text-gray-800 px-1 py-0.5 rounded text-xs font-mono;
+}
+
+.markdown-content pre {
+ @apply bg-gray-900 text-gray-100 p-3 rounded-md my-2 overflow-x-auto;
+}
+
+.markdown-content pre code {
+ @apply bg-transparent text-inherit p-0 text-xs;
+}
+
+.markdown-content table {
+ @apply w-full border-collapse my-2 text-xs;
+}
+
+.markdown-content th {
+ @apply bg-gray-50 border border-gray-300 px-2 py-1 text-left font-medium text-gray-700;
+}
+
+.markdown-content td {
+ @apply border border-gray-300 px-2 py-1 text-gray-900;
+}
+
+.markdown-content strong {
+ @apply font-semibold text-gray-900;
+}
+
+.markdown-content em {
+ @apply italic;
+}
+
+.markdown-content a {
+ @apply text-healthcare-blue underline hover:text-blue-800;
+}
+
+.markdown-content hr {
+ @apply border-t border-gray-300 my-3;
+}
+
+/* Syntax highlighting for code blocks */
+.markdown-content .hljs {
+ @apply bg-gray-900 text-gray-100;
+}
+
+.markdown-content .hljs-comment {
+ @apply text-gray-500;
+}
+
+.markdown-content .hljs-keyword {
+ @apply text-blue-400;
+}
+
+.markdown-content .hljs-string {
+ @apply text-green-400;
+}
+
+.markdown-content .hljs-number {
+ @apply text-yellow-400;
+}
+
+.markdown-content .hljs-function {
+ @apply text-purple-400;
+}
+
+/* Navigation styles */
+.nav-link-active {
+ @apply bg-gray-100 text-gray-900 font-medium;
+}
+
+.nav-link-inactive {
+ @apply text-gray-600 hover:bg-gray-50 hover:text-gray-900;
+}
+
+/* Form styles */
+.form-field-error {
+ @apply border-red-300 focus:border-red-500 focus:ring-red-500;
+}
+
+.form-error-message {
+ @apply text-sm text-red-600 mt-1;
+}
+
+/* Loading styles */
+.loading-skeleton {
+ @apply animate-pulse bg-gray-200 rounded;
+}
+
+/* Patient card styles */
+.patient-card {
+ @apply bg-white border border-gray-200 rounded-lg shadow-sm p-6 hover:shadow-md transition-shadow cursor-pointer;
+}
+
+.patient-card-priority-high {
+ @apply border-l-4 border-red-500;
+}
+
+.patient-card-priority-medium {
+ @apply border-l-4 border-yellow-500;
+}
+
+.patient-card-priority-low {
+ @apply border-l-4 border-green-500;
+}
+
+/* Document upload styles */
+.upload-zone {
+ @apply border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors;
+}
+
+.upload-zone-active {
+ @apply border-blue-500 bg-blue-50;
+}
+
+/* Accessibility improvements */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* Focus styles for better accessibility */
+.focus-visible {
+ @apply outline-2 outline-offset-2 outline-blue-600;
+}
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ .healthcare-card {
+ @apply bg-white border-2 border-gray-400 rounded-lg shadow-sm p-6;
+ }
+
+ .role-badge-doctor {
+ @apply bg-blue-200 text-blue-900 border border-blue-900;
+ }
+
+ .role-badge-nurse {
+ @apply bg-green-200 text-green-900 border border-green-900;
+ }
+
+ .role-badge-admin {
+ @apply bg-purple-200 text-purple-900 border border-purple-900;
+ }
+}
+
+/* Reduced motion support */
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* Print styles for medical records */
+@media print {
+ .no-print {
+ display: none !important;
+ }
+
+ .healthcare-card {
+ @apply bg-white border border-gray-400 rounded-lg shadow-sm p-6 break-inside-avoid;
+ }
+
+ .patient-card {
+ @apply break-inside-avoid mb-4;
+ }
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/components.json b/python/rag/healthcare-support-portal/frontend/components.json
new file mode 100644
index 00000000..80b736ff
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/components.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/styles/globals.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/package-lock.json b/python/rag/healthcare-support-portal/frontend/package-lock.json
new file mode 100644
index 00000000..71b3b58f
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/package-lock.json
@@ -0,0 +1,8602 @@
+{
+ "name": "frontend-service",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend-service",
+ "version": "0.1.0",
+ "dependencies": {
+ "@conform-to/react": "^1.8.2",
+ "@conform-to/zod": "^1.8.2",
+ "@hookform/resolvers": "^5.2.1",
+ "@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-checkbox": "^1.3.2",
+ "@radix-ui/react-dialog": "^1.1.14",
+ "@radix-ui/react-dropdown-menu": "^2.1.15",
+ "@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-progress": "^1.1.7",
+ "@radix-ui/react-scroll-area": "^1.2.9",
+ "@radix-ui/react-select": "^2.2.5",
+ "@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-tabs": "^1.1.12",
+ "@radix-ui/react-toast": "^1.2.14",
+ "@radix-ui/react-tooltip": "^1.2.7",
+ "@react-router/node": "^7.8.0",
+ "@tanstack/react-query": "^5.84.2",
+ "axios": "^1.11.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "isbot": "^5",
+ "lucide-react": "^0.539.0",
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1",
+ "react-hook-form": "^7.62.0",
+ "react-markdown": "^10.1.0",
+ "react-router": "^7.8.0",
+ "rehype-highlight": "^7.0.2",
+ "rehype-sanitize": "^6.0.0",
+ "remark-gfm": "^4.0.1",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.3.1",
+ "tailwindcss": "^4.1.11",
+ "zod": "^3.25.0"
+ },
+ "devDependencies": {
+ "@react-router/dev": "^7.8.0",
+ "@tailwindcss/vite": "^4.1.11",
+ "@types/node": "^24.2.1",
+ "@types/react": "^19.1.9",
+ "@types/react-dom": "^19.1.7",
+ "@typescript-eslint/eslint-plugin": "^8.39.0",
+ "@typescript-eslint/parser": "^8.39.0",
+ "@vitejs/plugin-react": "^4.7.0",
+ "eslint": "^9.33.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.20",
+ "typescript": "^5.9.2",
+ "vite": "^7.1.1",
+ "vite-tsconfig-paths": "^5.1.4"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
+ "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
+ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.0",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.27.3",
+ "@babel/helpers": "^7.27.6",
+ "@babel/parser": "^7.28.0",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.0",
+ "@babel/types": "^7.28.0",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
+ "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.0",
+ "@babel/types": "^7.28.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+ "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz",
+ "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/traverse": "^7.27.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
+ "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
+ "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+ "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
+ "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+ "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
+ "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
+ "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
+ "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
+ "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz",
+ "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz",
+ "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-syntax-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz",
+ "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/plugin-transform-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
+ "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.0",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
+ "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@conform-to/dom": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/@conform-to/dom/-/dom-1.8.2.tgz",
+ "integrity": "sha512-5eDqI9366+23Fl/E5zxebuy2FCebJKpSrdE/c6PZQ5Ul59BYQ17pBmkLkvxAKjPXW+Qq77BJyjGjuklwmqN08A==",
+ "license": "MIT"
+ },
+ "node_modules/@conform-to/react": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/@conform-to/react/-/react-1.8.2.tgz",
+ "integrity": "sha512-AJuHH4YY64o+VP3qsXd7bqlqCJYefTcEVj6mOiQr/m9iQV785gPtGE31jdmipvQ3ZHQE0Lp4e4th7kN/xo46qg==",
+ "license": "MIT",
+ "dependencies": {
+ "@conform-to/dom": "1.8.2"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@conform-to/zod": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/@conform-to/zod/-/zod-1.8.2.tgz",
+ "integrity": "sha512-uYipAoNByusRji7j84o40r9IvYmsV84utfCfvqr2b746/8JnBuoO6C90yltan5D9OVAJMIKejXW4T1XfOsaMWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@conform-to/dom": "1.8.2"
+ },
+ "peerDependencies": {
+ "zod": "^3.21.0 || ^4.0.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
+ "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
+ "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
+ "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
+ "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
+ "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
+ "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
+ "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
+ "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
+ "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
+ "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
+ "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
+ "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
+ "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
+ "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
+ "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
+ "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
+ "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
+ "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
+ "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
+ "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
+ "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
+ "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
+ "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
+ "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
+ "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
+ "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.15.2",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
+ "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.33.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz",
+ "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
+ "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.15.2",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz",
+ "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz",
+ "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.3"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT"
+ },
+ "node_modules/@hookform/resolvers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.1.tgz",
+ "integrity": "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/utils": "^0.3.0"
+ },
+ "peerDependencies": {
+ "react-hook-form": "^7.55.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.12",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
+ "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
+ "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.29",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
+ "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mjackson/node-fetch-server": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz",
+ "integrity": "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==",
+ "license": "MIT"
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@npmcli/git": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz",
+ "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/promise-spawn": "^6.0.0",
+ "lru-cache": "^7.4.4",
+ "npm-pick-manifest": "^8.0.0",
+ "proc-log": "^3.0.0",
+ "promise-inflight": "^1.0.1",
+ "promise-retry": "^2.0.1",
+ "semver": "^7.3.5",
+ "which": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@npmcli/package-json": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-4.0.1.tgz",
+ "integrity": "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^4.1.0",
+ "glob": "^10.2.2",
+ "hosted-git-info": "^6.1.1",
+ "json-parse-even-better-errors": "^3.0.0",
+ "normalize-package-data": "^5.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz",
+ "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "which": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
+ "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz",
+ "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
+ "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
+ "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz",
+ "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.15",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
+ "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
+ "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz",
+ "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.7",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.10",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
+ "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+ "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
+ "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",
+ "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz",
+ "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz",
+ "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.7",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
+ "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz",
+ "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.10",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz",
+ "integrity": "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
+ "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.7",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
+ "node_modules/@react-router/dev": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.8.0.tgz",
+ "integrity": "sha512-5NA9yLZComM+kCD3zNPL3rjrAFjzzODY8hjAJlpz/6jpyXoF28W8QTSo8rxc56XVNLONM75Y5nq1wzeEcWFFKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.27.7",
+ "@babel/generator": "^7.27.5",
+ "@babel/parser": "^7.27.7",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/preset-typescript": "^7.27.1",
+ "@babel/traverse": "^7.27.7",
+ "@babel/types": "^7.27.7",
+ "@npmcli/package-json": "^4.0.1",
+ "@react-router/node": "7.8.0",
+ "@vitejs/plugin-react": "^4.5.2",
+ "@vitejs/plugin-rsc": "0.4.11",
+ "arg": "^5.0.1",
+ "babel-dead-code-elimination": "^1.0.6",
+ "chokidar": "^4.0.0",
+ "dedent": "^1.5.3",
+ "es-module-lexer": "^1.3.1",
+ "exit-hook": "2.2.1",
+ "isbot": "^5.1.11",
+ "jsesc": "3.0.2",
+ "lodash": "^4.17.21",
+ "pathe": "^1.1.2",
+ "picocolors": "^1.1.1",
+ "prettier": "^3.6.2",
+ "react-refresh": "^0.14.0",
+ "semver": "^7.3.7",
+ "set-cookie-parser": "^2.6.0",
+ "tinyglobby": "^0.2.14",
+ "valibot": "^0.41.0",
+ "vite-node": "^3.2.2"
+ },
+ "bin": {
+ "react-router": "bin.js"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@react-router/serve": "^7.8.0",
+ "react-router": "^7.8.0",
+ "typescript": "^5.1.0",
+ "vite": "^5.1.0 || ^6.0.0 || ^7.0.0",
+ "wrangler": "^3.28.2 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@react-router/serve": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ },
+ "wrangler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-router/node": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.8.0.tgz",
+ "integrity": "sha512-/FFN9vqI2EHPwDCHTvsMInhrYvwJ5SlCeyUr1oWUxH47JyYkooVFks5++M4VkrTgj2ZBsMjPPKy0xRNTQdtBDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@mjackson/node-fetch-server": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react-router": "7.8.0",
+ "typescript": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
+ "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz",
+ "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz",
+ "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz",
+ "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz",
+ "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz",
+ "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz",
+ "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz",
+ "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz",
+ "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz",
+ "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz",
+ "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz",
+ "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz",
+ "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz",
+ "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz",
+ "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz",
+ "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz",
+ "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz",
+ "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz",
+ "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz",
+ "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
+ "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "enhanced-resolve": "^5.18.1",
+ "jiti": "^2.4.2",
+ "lightningcss": "1.30.1",
+ "magic-string": "^0.30.17",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.11"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz",
+ "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.4",
+ "tar": "^7.4.3"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.11",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.11",
+ "@tailwindcss/oxide-darwin-x64": "4.1.11",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.11",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.11",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.11",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.11",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.11",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.11"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz",
+ "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz",
+ "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz",
+ "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz",
+ "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz",
+ "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz",
+ "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz",
+ "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz",
+ "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz",
+ "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz",
+ "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.4.3",
+ "@emnapi/runtime": "^1.4.3",
+ "@emnapi/wasi-threads": "^1.0.2",
+ "@napi-rs/wasm-runtime": "^0.2.11",
+ "@tybys/wasm-util": "^0.9.0",
+ "tslib": "^2.8.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz",
+ "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz",
+ "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz",
+ "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.1.11",
+ "@tailwindcss/oxide": "4.1.11",
+ "tailwindcss": "4.1.11"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7"
+ }
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.83.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz",
+ "integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.84.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.2.tgz",
+ "integrity": "sha512-cZadySzROlD2+o8zIfbD978p0IphuQzRWiiH3I2ugnTmz4jbjc0+TdibpwqxlzynEen8OulgAg+rzdNF37s7XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.83.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
+ "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.2.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
+ "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.10.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.1.9",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
+ "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.1.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
+ "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
+ }
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz",
+ "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.39.0",
+ "@typescript-eslint/type-utils": "8.39.0",
+ "@typescript-eslint/utils": "8.39.0",
+ "@typescript-eslint/visitor-keys": "8.39.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.39.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz",
+ "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.39.0",
+ "@typescript-eslint/types": "8.39.0",
+ "@typescript-eslint/typescript-estree": "8.39.0",
+ "@typescript-eslint/visitor-keys": "8.39.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz",
+ "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.39.0",
+ "@typescript-eslint/types": "^8.39.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz",
+ "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.39.0",
+ "@typescript-eslint/visitor-keys": "8.39.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz",
+ "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz",
+ "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.39.0",
+ "@typescript-eslint/typescript-estree": "8.39.0",
+ "@typescript-eslint/utils": "8.39.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz",
+ "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz",
+ "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.39.0",
+ "@typescript-eslint/tsconfig-utils": "8.39.0",
+ "@typescript-eslint/types": "8.39.0",
+ "@typescript-eslint/visitor-keys": "8.39.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz",
+ "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.39.0",
+ "@typescript-eslint/types": "8.39.0",
+ "@typescript-eslint/typescript-estree": "8.39.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz",
+ "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.39.0",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react/node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-rsc": {
+ "version": "0.4.11",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-rsc/-/plugin-rsc-0.4.11.tgz",
+ "integrity": "sha512-+4H4wLi+Y9yF58znBfKgGfX8zcqUGt8ngnmNgzrdGdF1SVz7EO0sg7WnhK5fFVHt6fUxsVEjmEabsCWHKPL1Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@mjackson/node-fetch-server": "^0.7.0",
+ "es-module-lexer": "^1.7.0",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17",
+ "periscopic": "^4.0.2",
+ "turbo-stream": "^3.1.0",
+ "vitefu": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-dom": "*",
+ "vite": "*"
+ }
+ },
+ "node_modules/@vitejs/plugin-rsc/node_modules/@mjackson/node-fetch-server": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.7.0.tgz",
+ "integrity": "sha512-un8diyEBKU3BTVj3GzlTPA1kIjCkGdD+AMYQy31Gf9JCkfoZzwgJ79GUtHrF2BN3XPNMLpubbzPcxys+a3uZEw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/babel-dead-code-elimination": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz",
+ "integrity": "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.23.7",
+ "@babel/parser": "^7.23.6",
+ "@babel/traverse": "^7.23.7",
+ "@babel/types": "^7.23.6"
+ }
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.25.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
+ "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001726",
+ "electron-to-chromium": "^1.5.173",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001731",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
+ "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cross-spawn/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
+ "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/dedent": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz",
+ "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.192",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz",
+ "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.2",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
+ "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
+ "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.8",
+ "@esbuild/android-arm": "0.25.8",
+ "@esbuild/android-arm64": "0.25.8",
+ "@esbuild/android-x64": "0.25.8",
+ "@esbuild/darwin-arm64": "0.25.8",
+ "@esbuild/darwin-x64": "0.25.8",
+ "@esbuild/freebsd-arm64": "0.25.8",
+ "@esbuild/freebsd-x64": "0.25.8",
+ "@esbuild/linux-arm": "0.25.8",
+ "@esbuild/linux-arm64": "0.25.8",
+ "@esbuild/linux-ia32": "0.25.8",
+ "@esbuild/linux-loong64": "0.25.8",
+ "@esbuild/linux-mips64el": "0.25.8",
+ "@esbuild/linux-ppc64": "0.25.8",
+ "@esbuild/linux-riscv64": "0.25.8",
+ "@esbuild/linux-s390x": "0.25.8",
+ "@esbuild/linux-x64": "0.25.8",
+ "@esbuild/netbsd-arm64": "0.25.8",
+ "@esbuild/netbsd-x64": "0.25.8",
+ "@esbuild/openbsd-arm64": "0.25.8",
+ "@esbuild/openbsd-x64": "0.25.8",
+ "@esbuild/openharmony-arm64": "0.25.8",
+ "@esbuild/sunos-x64": "0.25.8",
+ "@esbuild/win32-arm64": "0.25.8",
+ "@esbuild/win32-ia32": "0.25.8",
+ "@esbuild/win32-x64": "0.25.8"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.33.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz",
+ "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.0",
+ "@eslint/config-helpers": "^0.3.1",
+ "@eslint/core": "^0.15.2",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.33.0",
+ "@eslint/plugin-kit": "^0.3.5",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.20",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz",
+ "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/exit-hook": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
+ "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globrex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hast-util-is-element": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
+ "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-sanitize": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
+ "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "unist-util-position": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-text": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
+ "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "hast-util-is-element": "^3.0.0",
+ "unist-util-find-after": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz",
+ "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^7.5.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/hosted-git-info/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
+ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
+ "license": "MIT"
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-reference": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+ "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.6"
+ }
+ },
+ "node_modules/isbot": {
+ "version": "5.1.29",
+ "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.29.tgz",
+ "integrity": "sha512-DelDWWoa3mBoyWTq3wjp+GIWx/yZdN7zLUE7NFhKjAiJ+uJVRkbLlwykdduCE4sPUUy8mlTYTmdhBUYu91F+sw==",
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
+ "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+ "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz",
+ "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
+ "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-darwin-arm64": "1.30.1",
+ "lightningcss-darwin-x64": "1.30.1",
+ "lightningcss-freebsd-x64": "1.30.1",
+ "lightningcss-linux-arm-gnueabihf": "1.30.1",
+ "lightningcss-linux-arm64-gnu": "1.30.1",
+ "lightningcss-linux-arm64-musl": "1.30.1",
+ "lightningcss-linux-x64-gnu": "1.30.1",
+ "lightningcss-linux-x64-musl": "1.30.1",
+ "lightningcss-win32-arm64-msvc": "1.30.1",
+ "lightningcss-win32-x64-msvc": "1.30.1"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
+ "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
+ "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
+ "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
+ "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
+ "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
+ "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
+ "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
+ "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
+ "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
+ "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lowlight": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
+ "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "devlop": "^1.0.0",
+ "highlight.js": "~11.11.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.539.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.539.0.tgz",
+ "integrity": "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
+ "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
+ "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
+ "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+ "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "dist/cjs/src/bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-package-data": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz",
+ "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^6.0.0",
+ "is-core-module": "^2.8.1",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-install-checks": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz",
+ "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz",
+ "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-package-arg": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz",
+ "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "hosted-git-info": "^6.0.0",
+ "proc-log": "^3.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^5.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-pick-manifest": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz",
+ "integrity": "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-install-checks": "^6.0.0",
+ "npm-normalize-package-bin": "^3.0.0",
+ "npm-package-arg": "^10.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/periscopic": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-4.0.2.tgz",
+ "integrity": "sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*",
+ "is-reference": "^3.0.2",
+ "zimmerframe": "^1.0.0"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/proc-log": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
+ "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+ "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "19.1.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
+ "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.1.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
+ "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.26.0"
+ },
+ "peerDependencies": {
+ "react": "^19.1.1"
+ }
+ },
+ "node_modules/react-hook-form": {
+ "version": "7.62.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
+ "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
+ "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
+ "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz",
+ "integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/rehype-highlight": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
+ "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-to-text": "^4.0.0",
+ "lowlight": "^3.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rehype-sanitize": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
+ "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-sanitize": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
+ "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.46.2",
+ "@rollup/rollup-android-arm64": "4.46.2",
+ "@rollup/rollup-darwin-arm64": "4.46.2",
+ "@rollup/rollup-darwin-x64": "4.46.2",
+ "@rollup/rollup-freebsd-arm64": "4.46.2",
+ "@rollup/rollup-freebsd-x64": "4.46.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.46.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.46.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.46.2",
+ "@rollup/rollup-linux-arm64-musl": "4.46.2",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.46.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.46.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.46.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.46.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.46.2",
+ "@rollup/rollup-linux-x64-gnu": "4.46.2",
+ "@rollup/rollup-linux-x64-musl": "4.46.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.46.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.46.2",
+ "@rollup/rollup-win32-x64-msvc": "4.46.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sonner": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
+ "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "dev": true,
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.21",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
+ "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/style-to-js": {
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz",
+ "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.9"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz",
+ "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.4"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
+ "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
+ "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
+ "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
+ "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.0.1",
+ "mkdirp": "^3.0.1",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tsconfck": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz",
+ "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tsconfck": "bin/tsconfck.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/turbo-stream": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-3.1.0.tgz",
+ "integrity": "sha512-tVI25WEXl4fckNEmrq70xU1XumxUwEx/FZD5AgEcV8ri7Wvrg2o7GEq8U7htrNx3CajciGm+kDyhRf5JB6t7/A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.10.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-find-after": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
+ "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
+ "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
+ "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
+ "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/valibot": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.41.0.tgz",
+ "integrity": "sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/validate-npm-package-name": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz",
+ "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.1.tgz",
+ "integrity": "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.6",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.14"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite-node/node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite-tsconfig-paths": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz",
+ "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "globrex": "^0.1.2",
+ "tsconfck": "^3.0.3"
+ },
+ "peerDependencies": {
+ "vite": "*"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitefu": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz",
+ "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "tests/deps/*",
+ "tests/projects/*",
+ "tests/projects/workspace/packages/*"
+ ],
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz",
+ "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zimmerframe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
+ "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/python/rag/healthcare-support-portal/frontend/package.json b/python/rag/healthcare-support-portal/frontend/package.json
new file mode 100644
index 00000000..b8afe5b9
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/package.json
@@ -0,0 +1,70 @@
+{
+ "private": true,
+ "type": "module",
+ "name": "frontend-service",
+ "version": "0.1.0",
+ "description": "React Router 7 Frontend for Healthcare Support Portal",
+ "scripts": {
+ "dev": "react-router dev",
+ "build": "react-router build",
+ "start": "react-router serve",
+ "typecheck": "tsc --noEmit",
+ "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
+ },
+ "dependencies": {
+ "@conform-to/react": "^1.8.2",
+ "@conform-to/zod": "^1.8.2",
+ "@hookform/resolvers": "^5.2.1",
+ "@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-checkbox": "^1.3.2",
+ "@radix-ui/react-dialog": "^1.1.14",
+ "@radix-ui/react-dropdown-menu": "^2.1.15",
+ "@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-progress": "^1.1.7",
+ "@radix-ui/react-scroll-area": "^1.2.9",
+ "@radix-ui/react-select": "^2.2.5",
+ "@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-tabs": "^1.1.12",
+ "@radix-ui/react-toast": "^1.2.14",
+ "@radix-ui/react-tooltip": "^1.2.7",
+ "@react-router/node": "^7.8.0",
+ "@tanstack/react-query": "^5.84.2",
+ "axios": "^1.11.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "isbot": "^5",
+ "lucide-react": "^0.539.0",
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1",
+ "react-hook-form": "^7.62.0",
+ "react-markdown": "^10.1.0",
+ "react-router": "^7.8.0",
+ "rehype-highlight": "^7.0.2",
+ "rehype-sanitize": "^6.0.0",
+ "remark-gfm": "^4.0.1",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.3.1",
+ "tailwindcss": "^4.1.11",
+ "zod": "^3.25.0"
+ },
+ "devDependencies": {
+ "@react-router/dev": "^7.8.0",
+ "@tailwindcss/vite": "^4.1.11",
+ "@types/node": "^24.2.1",
+ "@types/react": "^19.1.9",
+ "@types/react-dom": "^19.1.7",
+ "@typescript-eslint/eslint-plugin": "^8.39.0",
+ "@typescript-eslint/parser": "^8.39.0",
+ "@vitejs/plugin-react": "^4.7.0",
+ "eslint": "^9.33.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.20",
+ "typescript": "^5.9.2",
+ "vite": "^7.1.1",
+ "vite-tsconfig-paths": "^5.1.4"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+}
diff --git a/python/rag/healthcare-support-portal/frontend/react-router.config.ts b/python/rag/healthcare-support-portal/frontend/react-router.config.ts
new file mode 100644
index 00000000..2a7afab4
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/react-router.config.ts
@@ -0,0 +1,5 @@
+import type { Config } from "@react-router/dev/config";
+
+export default {
+ ssr: true,
+} satisfies Config;
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/run.sh b/python/rag/healthcare-support-portal/frontend/run.sh
new file mode 100755
index 00000000..9f72bca7
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/run.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+# run.sh - Start Healthcare Support Portal Frontend Service
+
+# Exit on any error
+set -e
+
+# Function to check if a command exists
+command_exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+echo "🌐 Starting Healthcare Support Portal Frontend Service..."
+
+# Check for Node.js and npm
+if ! command_exists node; then
+ echo "❌ Node.js not found. Please install Node.js 20.19.0+ and try again."
+ exit 1
+fi
+
+if ! command_exists npm; then
+ echo "❌ npm not found. Please install npm and try again."
+ exit 1
+fi
+
+# Check Node.js version
+NODE_VERSION=$(node --version | sed 's/v//')
+if [ "$(printf '%s\n' "20.19.0" "$NODE_VERSION" | sort -V | head -n1)" != "20.19.0" ]; then
+ echo "❌ Node.js $NODE_VERSION found, but 20.19.0+ is required."
+ exit 1
+fi
+echo "✅ Node.js $NODE_VERSION is compatible"
+
+# Check if node_modules exists
+if [ ! -d "node_modules" ]; then
+ echo "📦 Installing dependencies..."
+ if ! npm install; then
+ echo "❌ Failed to install dependencies"
+ exit 1
+ fi
+else
+ echo "✅ Dependencies found"
+fi
+
+# Check if port 3000 is available
+if command_exists lsof; then
+ if lsof -Pi :3000 -sTCP:LISTEN -t >/dev/null 2>&1; then
+ echo "❌ Port 3000 is already in use"
+ echo "Please stop the service using port 3000 or change the port in vite.config.ts"
+ exit 1
+ fi
+else
+ echo "⚠️ Warning: lsof not found. Cannot check if port 3000 is available."
+fi
+
+echo "🚀 Starting development server on http://localhost:3000"
+if ! npm run dev; then
+ echo "❌ Failed to start frontend development server"
+ exit 1
+fi
diff --git a/python/rag/healthcare-support-portal/frontend/tsconfig.json b/python/rag/healthcare-support-portal/frontend/tsconfig.json
new file mode 100644
index 00000000..614b94ce
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "include": ["**/*",
+ "**/.server/**/*",
+ "**/.client/**/*",
+ ".react-router/types/**/*"],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["node", "vite/client"],
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "rootDirs": [".", "./.react-router/types"],
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./app/*"]
+ },
+ "esModuleInterop": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "strict": true
+ }
+}
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/frontend/vite.config.ts b/python/rag/healthcare-support-portal/frontend/vite.config.ts
new file mode 100644
index 00000000..e566ffd2
--- /dev/null
+++ b/python/rag/healthcare-support-portal/frontend/vite.config.ts
@@ -0,0 +1,12 @@
+import { reactRouter } from "@react-router/dev/vite";
+import tailwindcss from "@tailwindcss/vite";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+ plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
+ server: {
+ port: 3000,
+ host: true
+ }
+});
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/packages/auth/.env.example b/python/rag/healthcare-support-portal/packages/auth/.env.example
new file mode 100644
index 00000000..77aa0ca2
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/auth/.env.example
@@ -0,0 +1,7 @@
+# Auth Service Environment Variables
+DEBUG=true
+SECRET_KEY=change-me-in-production
+DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare
+ACCESS_TOKEN_EXPIRE_MINUTES=30
+OSO_URL=http://localhost:8080
+OSO_AUTH=e_0123456789_12345_osotesttoken01xiIn
diff --git a/python/rag/healthcare-support-portal/packages/auth/.gitignore b/python/rag/healthcare-support-portal/packages/auth/.gitignore
new file mode 100644
index 00000000..a025cc96
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/auth/.gitignore
@@ -0,0 +1,7 @@
+# Auth Service Specific
+.env
+*.log
+logs/
+.pytest_cache/
+__pycache__/
+*.pyc
diff --git a/python/rag/healthcare-support-portal/packages/auth/README.md b/python/rag/healthcare-support-portal/packages/auth/README.md
new file mode 100644
index 00000000..ddc439f8
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/auth/README.md
@@ -0,0 +1,212 @@
+# Authentication Service
+
+The Authentication Service handles user authentication, registration, and token management for the Healthcare Support Portal. It provides JWT-based authentication with role-based access control using Oso.
+
+## Features
+
+- **User Registration:** Create new healthcare professional accounts
+- **Authentication:** JWT token-based login system
+- **Authorization:** Role-based access control (doctor, nurse, admin)
+- **User Management:** CRUD operations with Oso policy enforcement
+- **Token Refresh:** Extend authentication sessions
+- **Password Security:** Bcrypt hashing for secure password storage
+
+## Quick Start
+
+### Prerequisites
+
+- Python 3.8+
+- PostgreSQL with the Healthcare Support Portal database
+- uv package manager
+
+### Installation
+
+```bash
+# From project root
+uv sync
+
+# Or from package directory
+cd packages/auth
+uv sync
+```
+
+### Environment Variables
+
+Create a `.env` file or set these environment variables:
+
+```env
+DEBUG=true
+SECRET_KEY=your-secret-key-here
+DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare
+ACCESS_TOKEN_EXPIRE_MINUTES=30
+```
+
+### Running the Service
+
+```bash
+# Set PYTHONPATH and run
+export PYTHONPATH="../common/src:$PYTHONPATH"
+uv run uvicorn src.auth_service.main:app --reload --port 8001
+
+# Or use the run script from package directory
+cd packages/auth
+./run.sh
+```
+
+The service will be available at http://localhost:8001
+
+### API Documentation
+
+Interactive API docs are available at:
+- Swagger UI: http://localhost:8001/docs
+- ReDoc: http://localhost:8001/redoc
+
+## API Endpoints
+
+### Authentication
+
+| Method | Endpoint | Description | Auth Required |
+|--------|----------|-------------|---------------|
+| POST | `/api/v1/auth/login` | User login | No |
+| POST | `/api/v1/auth/register` | User registration | No |
+| GET | `/api/v1/auth/me` | Get current user info | Yes |
+| POST | `/api/v1/auth/refresh` | Refresh access token | Yes |
+
+### User Management
+
+| Method | Endpoint | Description | Auth Required |
+|--------|----------|-------------|---------------|
+| GET | `/api/v1/users/` | List users (authorized) | Yes |
+| GET | `/api/v1/users/{user_id}` | Get specific user | Yes |
+| PUT | `/api/v1/users/{user_id}` | Update user | Yes |
+
+### Health Check
+
+| Method | Endpoint | Description | Auth Required |
+|--------|----------|-------------|---------------|
+| GET | `/health` | Service health check | No |
+| GET | `/` | Service info | No |
+
+## Example Usage
+
+### Register a New User
+
+```bash
+curl -X POST "http://localhost:8001/api/v1/auth/register" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "username": "dr_smith",
+ "email": "smith@hospital.com",
+ "password": "secure_password",
+ "role": "doctor",
+ "department": "cardiology"
+ }'
+```
+
+### Login
+
+```bash
+curl -X POST "http://localhost:8001/api/v1/auth/login" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "username=dr_smith&password=secure_password"
+```
+
+Response:
+```json
+{
+ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "token_type": "bearer"
+}
+```
+
+### Get Current User Info
+
+```bash
+curl -X GET "http://localhost:8001/api/v1/auth/me" \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN"
+```
+
+## User Roles
+
+The service supports three user roles with different permissions:
+
+- **Doctor:** Can read their own info, manage assigned patients
+- **Nurse:** Can read their own info, view patients in their department
+- **Admin:** Can read/write all users and resources
+
+## Authorization Policies
+
+Authorization is handled by Oso policies defined in `common/policies/authorization.polar`. Key rules:
+
+- Users can read their own information
+- Admins can read any user
+- Role-based access to other resources
+
+## Development
+
+### Project Structure
+
+```
+src/auth_service/
+├── __init__.py
+├── main.py # FastAPI application
+├── config.py # Configuration settings
+└── routers/
+ ├── __init__.py
+ ├── auth.py # Authentication endpoints
+ └── users.py # User management endpoints
+```
+
+### Dependencies
+
+Key dependencies include:
+- FastAPI: Web framework
+- SQLAlchemy: ORM for database operations
+- Oso: Authorization framework
+- python-jose: JWT handling
+- passlib: Password hashing
+- common: Shared models and utilities
+
+### Testing
+
+```bash
+# Run tests (if implemented)
+uv run pytest
+
+# Test imports
+uv run python -c "from common.models import User; print('Import successful!')"
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Import errors:** Ensure PYTHONPATH includes `../common/src`
+2. **Database connection:** Verify PostgreSQL is running and DATABASE_URL is correct
+3. **JWT errors:** Check SECRET_KEY is set and consistent across services
+
+### Logs
+
+The service logs to stdout. For debugging, set `DEBUG=true` in your environment.
+
+## Security Considerations
+
+- Use strong SECRET_KEY in production
+- Set appropriate CORS origins
+- Use HTTPS in production
+- Regularly rotate JWT secrets
+- Monitor for failed authentication attempts
+
+## Integration
+
+This service integrates with:
+- **Patient Service:** Provides authentication for patient management
+- **RAG Service:** Provides authentication for document operations
+- **Common Package:** Shared models, database, and utilities
+
+## Contributing
+
+1. Follow the existing code structure
+2. Add appropriate error handling
+3. Update this README for new features
+4. Ensure Oso policies are updated for new endpoints
diff --git a/python/rag/healthcare-support-portal/packages/auth/pyproject.toml b/python/rag/healthcare-support-portal/packages/auth/pyproject.toml
new file mode 100644
index 00000000..31f0901a
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/auth/pyproject.toml
@@ -0,0 +1,24 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "auth"
+version = "0.1.0"
+description = "Authentication service for Healthcare Support Portal"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+ "fastapi>=0.104.0",
+ "uvicorn[standard]>=0.24.0",
+ "pydantic>=2.0.0",
+ "python-jose[cryptography]>=3.3.0",
+ "passlib[bcrypt]>=1.7.4",
+ "common",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/auth_service"]
+
+[tool.uv.sources]
+common = { workspace = true }
diff --git a/python/rag/healthcare-support-portal/packages/auth/run.sh b/python/rag/healthcare-support-portal/packages/auth/run.sh
new file mode 100755
index 00000000..d57a20e4
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/auth/run.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+# packages/auth/run.sh
+
+# Exit on any error
+set -e
+
+# Function to check if a command exists
+command_exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+# Load .env file if it exists
+if [ -f .env ]; then
+ echo "Loading environment variables from .env file..."
+ set -o allexport
+ source .env
+ set +o allexport
+else
+ echo "⚠️ Warning: No .env file found. Using defaults."
+fi
+
+# Set PYTHONPATH to include common package
+export PYTHONPATH="../common/src:$PYTHONPATH"
+
+# Set default environment variables if not already set
+export SECRET_KEY="${SECRET_KEY:-your-secret-key-here}"
+export DATABASE_URL="${DATABASE_URL:-postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare}"
+export DEBUG="${DEBUG:-true}"
+export HOST="${HOST:-0.0.0.0}"
+export PORT="${PORT:-8001}"
+export ACCESS_TOKEN_EXPIRE_MINUTES="${ACCESS_TOKEN_EXPIRE_MINUTES:-30}"
+
+echo "🔐 Starting Auth Service on port $PORT..."
+echo "📊 Debug mode: $DEBUG"
+echo "🔑 Using SECRET_KEY: ${SECRET_KEY:0:10}..."
+echo "🗄️ Database: ${DATABASE_URL%%@*}@***"
+echo "⏰ Token expiry: $ACCESS_TOKEN_EXPIRE_MINUTES minutes"
+
+# Check for uv command
+UV_CMD="uv"
+if [ -f "/opt/homebrew/bin/uv" ]; then
+ UV_CMD="/opt/homebrew/bin/uv"
+fi
+
+if ! command_exists "$UV_CMD"; then
+ echo "❌ uv package manager not found. Please install uv and try again."
+ echo " Installation: curl -LsSf https://astral.sh/uv/install.sh | sh"
+ exit 1
+fi
+
+# Check if port is available
+if command_exists lsof; then
+ if lsof -i :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
+ echo "❌ Port $PORT is already in use. Please stop the service using this port."
+ exit 1
+ fi
+fi
+
+# Check essential environment variables
+if [ "$SECRET_KEY" = "your-secret-key-here" ] || [ "$SECRET_KEY" = "change-me-in-production" ]; then
+ echo "⚠️ WARNING: Using default SECRET_KEY. This is insecure for production!"
+fi
+
+# Run the auth service
+echo "🚀 Starting uvicorn server..."
+if ! $UV_CMD run uvicorn src.auth_service.main:app --reload --host $HOST --port $PORT; then
+ echo "❌ Failed to start Auth Service"
+ exit 1
+fi
diff --git a/python/rag/healthcare-support-portal/packages/auth/src/auth_service/__init__.py b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/__init__.py
new file mode 100644
index 00000000..ad699938
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/__init__.py
@@ -0,0 +1,6 @@
+"""
+Authentication Service for Healthcare Support Portal
+Handles user authentication, registration, and token management
+"""
+
+__version__ = "0.1.0"
diff --git a/python/rag/healthcare-support-portal/packages/auth/src/auth_service/config.py b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/config.py
new file mode 100644
index 00000000..a757c419
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/config.py
@@ -0,0 +1,33 @@
+import os
+
+from pydantic_settings import BaseSettings
+
+
+class Settings(BaseSettings):
+ app_name: str = "Healthcare Support Portal - Auth Service"
+ debug: bool = os.getenv("DEBUG", "False").lower() == "true"
+ host: str = "0.0.0.0"
+ port: int = 8001
+
+ # Database
+ database_url: str = os.getenv(
+ "DATABASE_URL",
+ "postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare",
+ )
+
+ # JWT
+ secret_key: str = os.getenv("SECRET_KEY", "your-secret-key-here")
+ access_token_expire_minutes: int = 30
+
+ # Oso Configuration
+ oso_url: str = os.getenv("OSO_URL", "http://localhost:8080")
+ oso_auth: str = os.getenv("OSO_AUTH", "e_0123456789_12345_01xiIn")
+
+ class Config:
+ env_file = ".env"
+ # Explicitly ignore extra fields to prevent OpenTelemetry/Prometheus
+ # env vars from being loaded
+ extra = "ignore"
+
+
+settings = Settings()
diff --git a/python/rag/healthcare-support-portal/packages/auth/src/auth_service/main.py b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/main.py
new file mode 100644
index 00000000..6b00387e
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/main.py
@@ -0,0 +1,58 @@
+import sqlalchemy_oso_cloud
+from common.migration_check import require_migrations_current
+from common.models import Base
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from .config import settings
+from .routers import auth, users
+
+# Initialize SQLAlchemy Oso Cloud with registry and server settings
+sqlalchemy_oso_cloud.init(
+ Base.registry,
+ url=settings.oso_url,
+ api_key=settings.oso_auth,
+)
+
+# Create FastAPI app
+app = FastAPI(
+ title=settings.app_name,
+ description="Authentication and user management service",
+ version="0.1.0",
+ debug=settings.debug,
+)
+
+# Add CORS middleware
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # Configure appropriately for production
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Include routers
+app.include_router(auth.router, prefix="/api/v1/auth", tags=["Authentication"])
+app.include_router(users.router, prefix="/api/v1/users", tags=["Users"])
+
+
+@app.on_event("startup")
+async def startup_event():
+ """Verify migrations and start service"""
+ # Verify database migrations are current
+ require_migrations_current()
+ print(f"🚀 {settings.app_name} started on port {settings.port}")
+
+
+@app.get("/")
+async def root():
+ return {"service": "auth_service", "status": "healthy", "version": "0.1.0"}
+
+
+@app.get("/health")
+async def health_check():
+ return {"status": "healthy"}
+
+
+# Make Oso Cloud instance available to routes
+app.state.oso_sqlalchemy = sqlalchemy_oso_cloud
diff --git a/python/rag/healthcare-support-portal/packages/auth/src/auth_service/routers/__init__.py b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/routers/__init__.py
new file mode 100644
index 00000000..5bb3dc22
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/routers/__init__.py
@@ -0,0 +1 @@
+# Router package initialization
diff --git a/python/rag/healthcare-support-portal/packages/auth/src/auth_service/routers/auth.py b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/routers/auth.py
new file mode 100644
index 00000000..b129e426
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/routers/auth.py
@@ -0,0 +1,111 @@
+from datetime import timedelta
+
+from common.auth import (
+ create_access_token,
+ get_current_user,
+ get_password_hash,
+ verify_password,
+)
+from common.db import get_db
+from common.models import User
+from common.schemas import Token, UserCreate, UserResponse
+from fastapi import APIRouter, Depends, HTTPException, Request, status
+from fastapi.security import OAuth2PasswordRequestForm
+from sqlalchemy.orm import Session
+from sqlalchemy_oso_cloud import get_oso
+
+router = APIRouter()
+
+
+@router.post("/login", response_model=Token)
+async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
+ """
+ Authenticate user and return access token
+ """
+ # Find user by username
+ user = db.query(User).filter(User.username == form_data.username).first()
+
+ # Verify user exists and password is correct
+ if not user or not verify_password(form_data.password, user.hashed_password):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Incorrect username or password",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ # Check if user is active
+ if not user.is_active:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
+
+ # Create access token
+ access_token_expires = timedelta(minutes=30)
+ access_token = create_access_token(data={"sub": user.username}, expires_delta=access_token_expires)
+
+ return {"access_token": access_token, "token_type": "bearer"}
+
+
+@router.post("/register", response_model=UserResponse)
+async def register(
+ user_data: UserCreate,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Register a new user (admin only - deprecated, use POST /api/v1/users/ instead)
+ """
+ oso = get_oso()
+
+ # Create a temporary User object to check authorization
+ temp_user = User()
+
+ # Check if current user is authorized to write users (admin only)
+ if not oso.authorize(current_user, "write", temp_user):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Only administrators can create new users. Use POST /api/v1/users/ endpoint instead.",
+ )
+ # Check if user already exists
+ existing_user = db.query(User).filter((User.username == user_data.username) | (User.email == user_data.email)).first()
+
+ if existing_user:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Username or email already registered",
+ )
+
+ # Create new user
+ hashed_password = get_password_hash(user_data.password)
+ db_user = User(
+ username=user_data.username,
+ email=user_data.email,
+ hashed_password=hashed_password,
+ role=user_data.role,
+ department=user_data.department,
+ is_active=True,
+ )
+
+ db.add(db_user)
+ db.commit()
+ db.refresh(db_user)
+
+ return db_user
+
+
+@router.get("/me", response_model=UserResponse)
+async def get_current_user_info(current_user: User = Depends(get_current_user)):
+ """
+ Get current authenticated user information
+ """
+ return current_user
+
+
+@router.post("/refresh", response_model=Token)
+async def refresh_token(current_user: User = Depends(get_current_user)):
+ """
+ Refresh access token for authenticated user
+ """
+ access_token_expires = timedelta(minutes=30)
+ access_token = create_access_token(data={"sub": current_user.username}, expires_delta=access_token_expires)
+
+ return {"access_token": access_token, "token_type": "bearer"}
diff --git a/python/rag/healthcare-support-portal/packages/auth/src/auth_service/routers/users.py b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/routers/users.py
new file mode 100644
index 00000000..34f15fb7
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/auth/src/auth_service/routers/users.py
@@ -0,0 +1,165 @@
+from common.auth import get_current_user, get_password_hash
+from common.db import get_db
+from common.models import User
+from common.oso_sync import (
+ sync_admin_global_access,
+ sync_department_change,
+ sync_user_role_change,
+)
+from common.schemas import UserCreate, UserResponse
+from fastapi import APIRouter, Depends, HTTPException, Request, status
+from sqlalchemy.orm import Session
+
+router = APIRouter()
+
+
+@router.get("/", response_model=list[UserResponse])
+async def list_users(
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+ skip: int = 0,
+ limit: int = 100,
+):
+ """
+ List users (admin only - bypassing OSO for user management)
+ """
+ # Check if current user is admin - manual check instead of OSO
+ if current_user.role != "admin":
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Only administrators can list users",
+ )
+
+ # Get all users without OSO filtering since we already validated admin role
+ users = db.query(User).offset(skip).limit(limit).all()
+
+ return users
+
+
+@router.get("/{user_id}", response_model=UserResponse)
+async def get_user(
+ user_id: int,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Get specific user (admin only or self)
+ """
+ # Get the user
+ user = db.query(User).filter(User.id == user_id).first()
+ if not user:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
+
+ # Check authorization - admin can read any user, users can read themselves
+ if current_user.role != "admin" and current_user.id != user_id:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Not authorized to access this user",
+ )
+
+ return user
+
+
+@router.put("/{user_id}", response_model=UserResponse)
+async def update_user(
+ user_id: int,
+ user_update: UserCreate,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Update user (admin only)
+ """
+ # Get the user
+ user = db.query(User).filter(User.id == user_id).first()
+ if not user:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
+
+ # Check authorization - only admin can update users
+ if current_user.role != "admin":
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Only administrators can update users",
+ )
+
+ # Track changes for OSO sync
+ old_role = user.role
+ old_department = user.department
+
+ # Update user fields
+ user.username = user_update.username
+ user.email = user_update.email
+ user.role = user_update.role
+ user.department = user_update.department
+
+ db.commit()
+ db.refresh(user)
+
+ # Sync OSO facts if role or department changed
+ try:
+ if old_role != user.role:
+ sync_user_role_change(user, old_role)
+
+ if old_department != user.department:
+ sync_department_change(user, old_department)
+ except Exception as e:
+ # Log the error but don't fail the request
+ print(f"Warning: Failed to sync OSO facts for user {user.id}: {e}")
+
+ return user
+
+
+@router.post("/", response_model=UserResponse)
+async def create_user(
+ user_data: UserCreate,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Create new user (admin only)
+ """
+ # Check if current user has admin role (only admins can create users)
+ if current_user.role != "admin":
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Only administrators can create new users",
+ )
+
+ # Check if user already exists
+ existing_user = db.query(User).filter((User.username == user_data.username) | (User.email == user_data.email)).first()
+
+ if existing_user:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Username or email already registered",
+ )
+
+ # Create new user
+ hashed_password = get_password_hash(user_data.password)
+ new_user = User(
+ username=user_data.username,
+ email=user_data.email,
+ hashed_password=hashed_password,
+ role=user_data.role,
+ department=user_data.department,
+ is_active=True,
+ )
+
+ db.add(new_user)
+ db.commit()
+ db.refresh(new_user)
+
+ # Sync OSO facts for new user
+ try:
+ if new_user.role == "admin":
+ sync_admin_global_access(new_user)
+ print(f"Synced OSO facts for new user {new_user.username}")
+ except Exception as e:
+ # Log the error but don't fail the request
+ print(f"Warning: Failed to sync OSO facts for new user {new_user.id}: {e}")
+
+ return new_user
diff --git a/python/rag/healthcare-support-portal/packages/common/README.md b/python/rag/healthcare-support-portal/packages/common/README.md
new file mode 100644
index 00000000..d11673d2
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/README.md
@@ -0,0 +1,17 @@
+# Common Package
+
+This package contains shared components used across all microservices in the Healthcare Support Portal.
+
+## Contents
+
+- **Models** (`src/common/models.py`): SQLAlchemy database models for User, Patient, and Document entities
+- **Database utilities** (`src/common/db.py`): Database connection, table creation, and pgvector extension management
+- **Schemas** (`src/common/schemas.py`): Pydantic schemas for API request/response validation
+
+## Authorization
+
+Authorization policies are managed centrally by the Oso Dev Server and loaded from the project root `authorization.polar` file. Individual services connect to the Oso Dev Server at runtime rather than loading policy files locally.
+
+## Usage
+
+All services import shared models and utilities from this package. The package is added to the Python path via each service's `run.sh` script.
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/packages/common/alembic.ini b/python/rag/healthcare-support-portal/packages/common/alembic.ini
new file mode 100644
index 00000000..24b07769
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/alembic.ini
@@ -0,0 +1,148 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts.
+# this is typically a path given in POSIX (e.g. forward slashes)
+# format, relative to the token %(here)s which refers to the location of this
+# ini file
+script_location = %(here)s/alembic
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+# Uncomment the line below if you want the files to be prepended with date and time
+# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
+# for all available tokens
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory. for multiple paths, the path separator
+# is defined by "path_separator" below.
+prepend_sys_path = ../../src
+
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
+# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to ZoneInfo()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to /versions. When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "path_separator"
+# below.
+# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
+
+# path_separator; This indicates what character is used to split lists of file
+# paths, including version_locations and prepend_sys_path within configparser
+# files such as alembic.ini.
+# The default rendered in new alembic.ini files is "os", which uses os.pathsep
+# to provide os-dependent path splitting.
+#
+# Note that in order to support legacy alembic.ini files, this default does NOT
+# take place if path_separator is not present in alembic.ini. If this
+# option is omitted entirely, fallback logic is as follows:
+#
+# 1. Parsing of the version_locations option falls back to using the legacy
+# "version_path_separator" key, which if absent then falls back to the legacy
+# behavior of splitting on spaces and/or commas.
+# 2. Parsing of the prepend_sys_path option falls back to the legacy
+# behavior of splitting on spaces, commas, or colons.
+#
+# Valid values for path_separator are:
+#
+# path_separator = :
+# path_separator = ;
+# path_separator = space
+# path_separator = newline
+#
+# Use os.pathsep. Default configuration used for new projects.
+path_separator = os
+
+# set to 'true' to search source files recursively
+# in each "version_locations" directory
+# new in Alembic version 1.10
+# recursive_version_locations = false
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+# database URL. This is consumed by the user-maintained env.py script only.
+# other means of configuring database URLs may be customized within the env.py
+# file.
+# sqlalchemy.url will be set from environment variable in env.py
+# sqlalchemy.url = postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts. See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
+# hooks = ruff
+# ruff.type = module
+# ruff.module = ruff
+# ruff.options = check --fix REVISION_SCRIPT_FILENAME
+
+# Alternatively, use the exec runner to execute a binary found on your PATH
+# hooks = ruff
+# ruff.type = exec
+# ruff.executable = ruff
+# ruff.options = check --fix REVISION_SCRIPT_FILENAME
+
+# Logging configuration. This is also consumed by the user-maintained
+# env.py script only.
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARNING
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARNING
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/python/rag/healthcare-support-portal/packages/common/alembic/README b/python/rag/healthcare-support-portal/packages/common/alembic/README
new file mode 100644
index 00000000..98e4f9c4
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/alembic/README
@@ -0,0 +1 @@
+Generic single-database configuration.
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/packages/common/alembic/env.py b/python/rag/healthcare-support-portal/packages/common/alembic/env.py
new file mode 100644
index 00000000..3b7f72c4
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/alembic/env.py
@@ -0,0 +1,114 @@
+import os
+from logging.config import fileConfig
+
+from alembic import context
+from sqlalchemy import engine_from_config, pool, text
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+# Import models to ensure they're registered with SQLAlchemy
+# This is crucial for autogenerate to work
+from common.models import Base
+
+# Set the target metadata for autogenerate support
+target_metadata = Base.metadata
+
+# Get database URL from environment variable or use default
+DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare")
+
+# Override the ini file's sqlalchemy.url with environment variable
+config.set_main_option("sqlalchemy.url", DATABASE_URL)
+
+# Define naming conventions for constraints
+# This ensures consistent naming across migrations
+naming_convention = {
+ "ix": "ix_%(column_0_label)s",
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s",
+}
+
+target_metadata.naming_convention = naming_convention
+
+
+def include_object(object, name, type_, reflected, compare_to):
+ """Filter out system objects from migrations"""
+ # Skip any PostgreSQL internal schemas
+ if type_ == "schema":
+ return name not in ["information_schema", "pg_catalog", "pg_toast"]
+ return True
+
+
+def run_migrations_offline() -> None:
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ include_object=include_object,
+ compare_type=True,
+ compare_server_default=True,
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online() -> None:
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ configuration = config.get_section(config.config_ini_section)
+ configuration["sqlalchemy.url"] = DATABASE_URL
+
+ connectable = engine_from_config(
+ configuration,
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ with connectable.connect() as connection:
+ # Enable pgvector extension if not exists
+ # This needs to be done before migrations run
+ connection.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
+ connection.commit()
+
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata,
+ include_object=include_object,
+ compare_type=True,
+ compare_server_default=True,
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/python/rag/healthcare-support-portal/packages/common/alembic/script.py.mako b/python/rag/healthcare-support-portal/packages/common/alembic/script.py.mako
new file mode 100644
index 00000000..11016301
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/alembic/script.py.mako
@@ -0,0 +1,28 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision: str = ${repr(up_revision)}
+down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
+branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
+depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ """Upgrade schema."""
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ """Downgrade schema."""
+ ${downgrades if downgrades else "pass"}
diff --git a/python/rag/healthcare-support-portal/packages/common/alembic/versions/20250808_1321-fc1dc9787b9f_initial_schema_with_users_patients_.py b/python/rag/healthcare-support-portal/packages/common/alembic/versions/20250808_1321-fc1dc9787b9f_initial_schema_with_users_patients_.py
new file mode 100644
index 00000000..66b93db3
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/alembic/versions/20250808_1321-fc1dc9787b9f_initial_schema_with_users_patients_.py
@@ -0,0 +1,141 @@
+"""Initial schema with users, patients, documents, and embeddings
+
+Revision ID: fc1dc9787b9f
+Revises:
+Create Date: 2025-08-08 13:21:35.737581
+
+"""
+
+from collections.abc import Sequence
+
+import pgvector.sqlalchemy
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "fc1dc9787b9f"
+down_revision: str | Sequence[str] | None = None
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ """Upgrade schema."""
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table(
+ "users",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("username", sa.String(length=50), nullable=False),
+ sa.Column("email", sa.String(length=100), nullable=False),
+ sa.Column("hashed_password", sa.String(length=255), nullable=False),
+ sa.Column("role", sa.String(length=50), nullable=False),
+ sa.Column("department", sa.String(length=100), nullable=True),
+ sa.Column("is_active", sa.Boolean(), nullable=True),
+ sa.Column(
+ "created_at",
+ sa.DateTime(timezone=True),
+ server_default=sa.text("now()"),
+ nullable=True,
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
+ op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False)
+ op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True)
+ op.create_table(
+ "patients",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("name", sa.String(length=100), nullable=False),
+ sa.Column("date_of_birth", sa.DateTime(), nullable=True),
+ sa.Column("medical_record_number", sa.String(length=50), nullable=True),
+ sa.Column("department", sa.String(length=100), nullable=True),
+ sa.Column("assigned_doctor_id", sa.Integer(), nullable=True),
+ sa.Column("is_active", sa.Boolean(), nullable=True),
+ sa.Column(
+ "created_at",
+ sa.DateTime(timezone=True),
+ server_default=sa.text("now()"),
+ nullable=True,
+ ),
+ sa.ForeignKeyConstraint(
+ ["assigned_doctor_id"],
+ ["users.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_index(op.f("ix_patients_id"), "patients", ["id"], unique=False)
+ op.create_index(
+ op.f("ix_patients_medical_record_number"),
+ "patients",
+ ["medical_record_number"],
+ unique=True,
+ )
+ op.create_table(
+ "documents",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("title", sa.String(length=200), nullable=False),
+ sa.Column("content", sa.Text(), nullable=False),
+ sa.Column("document_type", sa.String(length=50), nullable=True),
+ sa.Column("patient_id", sa.Integer(), nullable=True),
+ sa.Column("department", sa.String(length=100), nullable=True),
+ sa.Column("created_by_id", sa.Integer(), nullable=True),
+ sa.Column("is_sensitive", sa.Boolean(), nullable=True),
+ sa.Column(
+ "created_at",
+ sa.DateTime(timezone=True),
+ server_default=sa.text("now()"),
+ nullable=True,
+ ),
+ sa.ForeignKeyConstraint(
+ ["created_by_id"],
+ ["users.id"],
+ ),
+ sa.ForeignKeyConstraint(
+ ["patient_id"],
+ ["patients.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_index(op.f("ix_documents_id"), "documents", ["id"], unique=False)
+ op.create_table(
+ "embeddings",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("document_id", sa.Integer(), nullable=False),
+ sa.Column("content_chunk", sa.Text(), nullable=False),
+ sa.Column(
+ "embedding_vector",
+ pgvector.sqlalchemy.vector.VECTOR(dim=1536),
+ nullable=True,
+ ),
+ sa.Column("chunk_index", sa.Integer(), nullable=True),
+ sa.Column(
+ "created_at",
+ sa.DateTime(timezone=True),
+ server_default=sa.text("now()"),
+ nullable=True,
+ ),
+ sa.ForeignKeyConstraint(
+ ["document_id"],
+ ["documents.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_index(op.f("ix_embeddings_id"), "embeddings", ["id"], unique=False)
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ """Downgrade schema."""
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f("ix_embeddings_id"), table_name="embeddings")
+ op.drop_table("embeddings")
+ op.drop_index(op.f("ix_documents_id"), table_name="documents")
+ op.drop_table("documents")
+ op.drop_index(op.f("ix_patients_medical_record_number"), table_name="patients")
+ op.drop_index(op.f("ix_patients_id"), table_name="patients")
+ op.drop_table("patients")
+ op.drop_index(op.f("ix_users_username"), table_name="users")
+ op.drop_index(op.f("ix_users_id"), table_name="users")
+ op.drop_index(op.f("ix_users_email"), table_name="users")
+ op.drop_table("users")
+ # ### end Alembic commands ###
diff --git a/python/rag/healthcare-support-portal/packages/common/pyproject.toml b/python/rag/healthcare-support-portal/packages/common/pyproject.toml
new file mode 100644
index 00000000..aaa4c476
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/pyproject.toml
@@ -0,0 +1,30 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "common"
+version = "0.1.0"
+description = "Shared models, utilities, and policies for Healthcare Support Portal"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+ "fastapi>=0.104.0",
+ "sqlalchemy>=2.0.0",
+ "psycopg2-binary>=2.9.0",
+ "oso-cloud",
+ "sqlalchemy-oso-cloud",
+ "pydantic>=2.0.0",
+ "pydantic-settings>=2.0.0",
+ "passlib[bcrypt]>=1.7.4",
+ "bcrypt>=4.0.0,<5.0.0",
+ "python-jose[cryptography]>=3.3.0",
+ "pgvector>=0.2.0",
+ "alembic>=1.16.0",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/common"]
+
+[tool.hatch.build]
+include = ["src/common"]
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/packages/common/src/common/__init__.py b/python/rag/healthcare-support-portal/packages/common/src/common/__init__.py
new file mode 100644
index 00000000..066f74f5
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/src/common/__init__.py
@@ -0,0 +1,22 @@
+"""
+Common package for Healthcare Support Portal
+Shared models, database utilities, and policies
+"""
+
+from .auth import create_access_token, get_current_user, verify_password
+from .db import SessionLocal, engine, get_db
+from .models import Base, Document, Embedding, Patient, User
+
+__all__ = [
+ "Base",
+ "User",
+ "Patient",
+ "Document",
+ "Embedding",
+ "get_db",
+ "engine",
+ "SessionLocal",
+ "get_current_user",
+ "create_access_token",
+ "verify_password",
+]
diff --git a/python/rag/healthcare-support-portal/packages/common/src/common/auth.py b/python/rag/healthcare-support-portal/packages/common/src/common/auth.py
new file mode 100644
index 00000000..efc23337
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/src/common/auth.py
@@ -0,0 +1,86 @@
+import os
+from datetime import datetime, timedelta
+
+from fastapi import Depends, HTTPException, status
+from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
+from jose import JWTError, jwt
+from passlib.context import CryptContext
+from sqlalchemy.orm import Session
+
+from .db import get_db
+from .models import User
+
+# Security configuration
+SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-here")
+ALGORITHM = "HS256"
+ACCESS_TOKEN_EXPIRE_MINUTES = 30
+
+# Password hashing
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+# HTTP Bearer token scheme
+security = HTTPBearer()
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+ """Verify a plain password against a hashed password."""
+ return pwd_context.verify(plain_password, hashed_password)
+
+
+def get_password_hash(password: str) -> str:
+ """Hash a password."""
+ return pwd_context.hash(password)
+
+
+def create_access_token(data: dict, expires_delta: timedelta | None = None):
+ """Create a JWT access token."""
+ to_encode = data.copy()
+ if expires_delta:
+ expire = datetime.utcnow() + expires_delta
+ else:
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+
+ to_encode.update({"exp": expire})
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+ return encoded_jwt
+
+
+def get_current_user(
+ credentials: HTTPAuthorizationCredentials = Depends(security),
+ db: Session = Depends(get_db),
+) -> User:
+ """Get the current authenticated user from JWT token."""
+ credentials_exception = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ try:
+ payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
+ username: str = payload.get("sub")
+ if username is None:
+ raise credentials_exception
+ except JWTError:
+ raise credentials_exception
+
+ user = db.query(User).filter(User.username == username).first()
+ if user is None:
+ raise credentials_exception
+
+ return user
+
+
+# Development mode authentication bypass
+def get_current_user_dev(
+ db: Session = Depends(get_db),
+) -> User:
+ """Development mode - automatically authenticate as dr_smith for testing."""
+ # This should only be used in development
+ user = db.query(User).filter(User.username == "dr_smith").first()
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Development user 'dr_smith' not found in database",
+ )
+ return user
diff --git a/python/rag/healthcare-support-portal/packages/common/src/common/db.py b/python/rag/healthcare-support-portal/packages/common/src/common/db.py
new file mode 100644
index 00000000..431dbbdb
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/src/common/db.py
@@ -0,0 +1,22 @@
+import os
+
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+# Database configuration
+DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare")
+
+# Create engine
+engine = create_engine(DATABASE_URL, echo=True)
+
+# Create SessionLocal class
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+
+
+# Dependency to get database session
+def get_db():
+ db = SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
diff --git a/python/rag/healthcare-support-portal/packages/common/src/common/migration_check.py b/python/rag/healthcare-support-portal/packages/common/src/common/migration_check.py
new file mode 100644
index 00000000..39a7e5bf
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/src/common/migration_check.py
@@ -0,0 +1,99 @@
+"""
+Migration verification utilities for services.
+Ensures database migrations are up-to-date before service startup.
+"""
+
+import os
+import sys
+from pathlib import Path
+
+from alembic.config import Config
+from alembic.runtime.migration import MigrationContext
+from alembic.script import ScriptDirectory
+from sqlalchemy import create_engine
+
+
+def get_alembic_config() -> Config:
+ """Get Alembic configuration from the common package."""
+ # Find the alembic.ini file in the common package
+ common_dir = Path(__file__).parent.parent.parent # Go up to packages/common
+ alembic_ini = common_dir / "alembic.ini"
+
+ if not alembic_ini.exists():
+ raise FileNotFoundError(f"alembic.ini not found at {alembic_ini}. " "Please ensure Alembic is properly initialized.")
+
+ config = Config(str(alembic_ini))
+
+ # Set the script location to absolute path
+ script_location = common_dir / "alembic"
+ config.set_main_option("script_location", str(script_location))
+
+ return config
+
+
+def verify_migrations_current(database_url: str = None) -> bool:
+ """
+ Verify that database migrations are up to date.
+
+ Args:
+ database_url: Database connection URL. If None, uses DATABASE_URL env var.
+
+ Returns:
+ True if migrations are current, False otherwise.
+ """
+ if database_url is None:
+ database_url = os.getenv(
+ "DATABASE_URL",
+ "postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare",
+ )
+
+ try:
+ # Create engine and get current database revision
+ engine = create_engine(database_url)
+
+ with engine.connect() as connection:
+ context = MigrationContext.configure(connection)
+ current_rev = context.get_current_revision()
+
+ # Get the head revision from Alembic scripts
+ config = get_alembic_config()
+ script = ScriptDirectory.from_config(config)
+ head_rev = script.get_current_head()
+
+ if current_rev != head_rev:
+ print("❌ Database migrations are out of date!")
+ print(f" Current revision: {current_rev or 'None (no migrations applied)'}")
+ print(f" Head revision: {head_rev}")
+ print("")
+ print(" To fix this, run one of the following:")
+ print(" 1. Docker Compose: docker-compose run migrate")
+ print(" 2. Manual: cd packages/common && uv run alembic upgrade head")
+ print("")
+ return False
+
+ print(f"✅ Database migrations are up to date (revision: {current_rev})")
+ return True
+
+ except Exception as e:
+ print(f"❌ Error checking migration status: {e}")
+ print(" This might indicate:")
+ print(" 1. Database is not accessible")
+ print(" 2. Migrations have not been initialized")
+ print(" 3. Database connection issues")
+ return False
+
+
+def require_migrations_current(database_url: str = None) -> None:
+ """
+ Require migrations to be current, exit if not.
+
+ This should be called at service startup to ensure
+ the database schema is up to date.
+
+ Args:
+ database_url: Database connection URL. If None, uses DATABASE_URL env var.
+ """
+ if not verify_migrations_current(database_url):
+ print("🛑 Service startup aborted due to outdated migrations.")
+ print(" Please run migrations before starting the service.")
+ sys.exit(1)
diff --git a/python/rag/healthcare-support-portal/packages/common/src/common/models.py b/python/rag/healthcare-support-portal/packages/common/src/common/models.py
new file mode 100644
index 00000000..2a4aa403
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/src/common/models.py
@@ -0,0 +1,85 @@
+from pgvector.sqlalchemy import Vector
+from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text
+from sqlalchemy.orm import declarative_base, relationship
+from sqlalchemy.sql import func
+from sqlalchemy_oso_cloud.oso import Resource
+
+Base = declarative_base()
+
+
+class User(Base, Resource):
+ __tablename__ = "users"
+
+ # OSO Cloud type identifier
+ type = "User"
+
+ id = Column(Integer, primary_key=True, index=True)
+ username = Column(String(50), unique=True, index=True, nullable=False)
+ email = Column(String(100), unique=True, index=True, nullable=False)
+ hashed_password = Column(String(255), nullable=False)
+ role = Column(String(50), nullable=False) # doctor, nurse, admin
+ department = Column(String(100))
+ is_active = Column(Boolean, default=True)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+
+ # Relationships
+ assigned_patients = relationship("Patient", back_populates="assigned_doctor")
+
+
+class Patient(Base, Resource):
+ __tablename__ = "patients"
+
+ # OSO Cloud type identifier
+ type = "Patient"
+
+ id = Column(Integer, primary_key=True, index=True)
+ name = Column(String(100), nullable=False)
+ date_of_birth = Column(DateTime)
+ medical_record_number = Column(String(50), unique=True, index=True)
+ department = Column(String(100))
+ assigned_doctor_id = Column(Integer, ForeignKey("users.id"))
+ is_active = Column(Boolean, default=True)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+
+ # Relationships
+ assigned_doctor = relationship("User", back_populates="assigned_patients")
+ documents = relationship("Document", back_populates="patient")
+
+
+class Document(Base, Resource):
+ __tablename__ = "documents"
+
+ # OSO Cloud type identifier
+ type = "Document"
+
+ id = Column(Integer, primary_key=True, index=True)
+ title = Column(String(200), nullable=False)
+ content = Column(Text, nullable=False)
+ document_type = Column(String(50)) # medical_record, protocol, policy, etc.
+ patient_id = Column(Integer, ForeignKey("patients.id"), nullable=True)
+ department = Column(String(100))
+ created_by_id = Column(Integer, ForeignKey("users.id"))
+ is_sensitive = Column(Boolean, default=False)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+
+ # Relationships
+ patient = relationship("Patient", back_populates="documents")
+ created_by = relationship("User")
+ embeddings = relationship("Embedding", back_populates="document")
+
+
+class Embedding(Base, Resource):
+ __tablename__ = "embeddings"
+
+ # OSO Cloud type identifier
+ type = "Embedding"
+
+ id = Column(Integer, primary_key=True, index=True)
+ document_id = Column(Integer, ForeignKey("documents.id"), nullable=False)
+ content_chunk = Column(Text, nullable=False)
+ embedding_vector = Column(Vector(1536)) # OpenAI embedding dimension
+ chunk_index = Column(Integer, default=0)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+
+ # Relationships
+ document = relationship("Document", back_populates="embeddings")
diff --git a/python/rag/healthcare-support-portal/packages/common/src/common/oso_sync.py b/python/rag/healthcare-support-portal/packages/common/src/common/oso_sync.py
new file mode 100644
index 00000000..59a4519f
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/src/common/oso_sync.py
@@ -0,0 +1,440 @@
+"""
+OSO Cloud Fact Synchronization Utilities
+Manages authorization facts in OSO Cloud to keep them in sync with database state.
+"""
+
+import logging
+
+try:
+ from oso_cloud import Value
+except ImportError:
+ print("Warning: oso_cloud not installed. OSO fact synchronization will be disabled.")
+ Value = None
+
+from .db import SessionLocal
+from .models import Document, Embedding, Patient, User
+
+logger = logging.getLogger(__name__)
+
+
+def get_oso_client():
+ """Get the OSO client instance from sqlalchemy-oso-cloud."""
+ if Value is None:
+ raise ImportError("oso_cloud is not available")
+
+ try:
+ from sqlalchemy_oso_cloud import get_oso
+
+ return get_oso()
+ except ImportError as e:
+ logger.error(f"Failed to import OSO client: {e}")
+ raise
+ except Exception as e:
+ logger.error(f"Failed to get OSO client: {e}")
+ raise
+
+
+def sync_admin_global_access(user: User) -> bool:
+ """
+ Sync global admin access facts for an admin user.
+ Creates has_role facts for admin access to all resources.
+ """
+ if user.role != "admin":
+ logger.warning(f"User {user.id} is not an admin, skipping global access sync")
+ return False
+
+ try:
+ oso = get_oso_client()
+ db = SessionLocal()
+
+ try:
+ with oso.batch() as tx:
+ user_value = Value("User", str(user.id))
+
+ # Admin access to all patients
+ patients = db.query(Patient).filter(Patient.is_active).all()
+ for patient in patients:
+ tx.insert(
+ (
+ "has_role",
+ user_value,
+ "admin",
+ Value("Patient", str(patient.id)),
+ )
+ )
+
+ # Admin access to all documents
+ documents = db.query(Document).all()
+ for doc in documents:
+ tx.insert(
+ (
+ "has_role",
+ user_value,
+ "admin",
+ Value("Document", str(doc.id)),
+ )
+ )
+
+ # Admin access to all embeddings
+ embeddings = db.query(Embedding).all()
+ for embed in embeddings:
+ tx.insert(
+ (
+ "has_role",
+ user_value,
+ "admin",
+ Value("Embedding", str(embed.id)),
+ )
+ )
+
+ # Admin access to all users
+ users = db.query(User).filter(User.is_active).all()
+ for other_user in users:
+ tx.insert(
+ (
+ "has_role",
+ user_value,
+ "admin",
+ Value("User", str(other_user.id)),
+ )
+ )
+
+ logger.info(f"Synced global admin access for user {user.id}")
+ return True
+
+ finally:
+ db.close()
+
+ except Exception as e:
+ logger.error(f"Failed to sync global admin access for user {user.id}: {e}")
+ return False
+
+
+def remove_admin_global_access(user: User) -> bool:
+ """Remove all admin access facts for a user."""
+ try:
+ oso = get_oso_client()
+
+ with oso.batch() as tx:
+ user_value = Value("User", str(user.id))
+ # Remove all admin roles for this user
+ tx.delete(("has_role", user_value, "admin", None))
+
+ logger.info(f"Removed all admin access for user {user.id}")
+ return True
+
+ except Exception as e:
+ logger.error(f"Failed to remove admin access for user {user.id}: {e}")
+ return False
+
+
+def sync_patient_access(patient: Patient) -> bool:
+ """
+ Sync patient access facts based on doctor assignment and department.
+ Creates assigned_doctor and department_nurse facts.
+ """
+ try:
+ oso = get_oso_client()
+ db = SessionLocal()
+
+ try:
+ with oso.batch() as tx:
+ patient_value = Value("Patient", str(patient.id))
+
+ # Doctor assignment fact
+ if patient.assigned_doctor_id:
+ doctor_value = Value("User", str(patient.assigned_doctor_id))
+ tx.insert(("has_role", doctor_value, "assigned_doctor", patient_value))
+
+ # Department nurse facts - all nurses in the patient's department
+ if patient.department:
+ nurses = (
+ db.query(User)
+ .filter(
+ User.role == "nurse",
+ User.department == patient.department,
+ User.is_active,
+ )
+ .all()
+ )
+
+ for nurse in nurses:
+ nurse_value = Value("User", str(nurse.id))
+ tx.insert(("has_role", nurse_value, "department_nurse", patient_value))
+
+ # Admin access facts for all admins
+ admins = db.query(User).filter(User.role == "admin", User.is_active).all()
+
+ for admin in admins:
+ admin_value = Value("User", str(admin.id))
+ tx.insert(("has_role", admin_value, "admin", patient_value))
+
+ logger.info(f"Synced patient access for patient {patient.id}")
+ return True
+
+ finally:
+ db.close()
+
+ except Exception as e:
+ logger.error(f"Failed to sync patient access for patient {patient.id}: {e}")
+ return False
+
+
+def remove_patient_access(patient_id: int) -> bool:
+ """Remove all access facts for a patient."""
+ try:
+ oso = get_oso_client()
+
+ with oso.batch() as tx:
+ patient_value = Value("Patient", str(patient_id))
+ # Remove all roles for this patient
+ tx.delete(("has_role", None, None, patient_value))
+
+ logger.info(f"Removed all access facts for patient {patient_id}")
+ return True
+
+ except Exception as e:
+ logger.error(f"Failed to remove patient access facts for patient {patient_id}: {e}")
+ return False
+
+
+def sync_document_access(document: Document) -> bool:
+ """
+ Sync document access facts based on ownership, patient relationships, and department.
+ """
+ try:
+ oso = get_oso_client()
+ db = SessionLocal()
+
+ try:
+ with oso.batch() as tx:
+ doc_value = Value("Document", str(document.id))
+
+ # Owner fact - document creator has full control
+ if document.created_by_id:
+ owner_value = Value("User", str(document.created_by_id))
+ tx.insert(("has_role", owner_value, "owner", doc_value))
+
+ # Patient doctor fact - if document is linked to a patient
+ if document.patient_id:
+ patient = db.query(Patient).filter(Patient.id == document.patient_id).first()
+ if patient and patient.assigned_doctor_id:
+ doctor_value = Value("User", str(patient.assigned_doctor_id))
+ tx.insert(("has_role", doctor_value, "patient_doctor", doc_value))
+
+ # Department staff fact - for non-sensitive documents
+ if document.department and not document.is_sensitive:
+ dept_users = db.query(User).filter(User.department == document.department, User.is_active).all()
+
+ for user in dept_users:
+ user_value = Value("User", str(user.id))
+ tx.insert(("has_role", user_value, "department_staff", doc_value))
+
+ # Admin access facts for all admins
+ admins = db.query(User).filter(User.role == "admin", User.is_active).all()
+
+ for admin in admins:
+ admin_value = Value("User", str(admin.id))
+ tx.insert(("has_role", admin_value, "admin", doc_value))
+
+ logger.info(f"Synced document access for document {document.id}")
+ return True
+
+ finally:
+ db.close()
+
+ except Exception as e:
+ logger.error(f"Failed to sync document access for document {document.id}: {e}")
+ return False
+
+
+def remove_document_access(document_id: int) -> bool:
+ """Remove all access facts for a document."""
+ try:
+ oso = get_oso_client()
+
+ with oso.batch() as tx:
+ doc_value = Value("Document", str(document_id))
+ # Remove all roles for this document
+ tx.delete(("has_role", None, None, doc_value))
+
+ logger.info(f"Removed all access facts for document {document_id}")
+ return True
+
+ except Exception as e:
+ logger.error(f"Failed to remove document access facts for document {document_id}: {e}")
+ return False
+
+
+def sync_embedding_access(embedding: Embedding) -> bool:
+ """
+ Sync embedding access facts - only admins can access embeddings.
+ """
+ try:
+ oso = get_oso_client()
+ db = SessionLocal()
+
+ try:
+ with oso.batch() as tx:
+ embed_value = Value("Embedding", str(embedding.id))
+
+ # Admin access facts for all admins
+ admins = db.query(User).filter(User.role == "admin", User.is_active).all()
+
+ for admin in admins:
+ admin_value = Value("User", str(admin.id))
+ tx.insert(("has_role", admin_value, "admin", embed_value))
+
+ logger.info(f"Synced embedding access for embedding {embedding.id}")
+ return True
+
+ finally:
+ db.close()
+
+ except Exception as e:
+ logger.error(f"Failed to sync embedding access for embedding {embedding.id}: {e}")
+ return False
+
+
+def remove_embedding_access(embedding_id: int) -> bool:
+ """Remove all access facts for an embedding."""
+ try:
+ oso = get_oso_client()
+
+ with oso.batch() as tx:
+ embed_value = Value("Embedding", str(embedding_id))
+ # Remove all roles for this embedding
+ tx.delete(("has_role", None, None, embed_value))
+
+ logger.info(f"Removed all access facts for embedding {embedding_id}")
+ return True
+
+ except Exception as e:
+ logger.error(f"Failed to remove embedding access facts for embedding {embedding_id}: {e}")
+ return False
+
+
+def sync_user_role_change(user: User, old_role: str | None = None) -> bool:
+ """
+ Handle user role changes - remove old role facts and add new ones.
+ """
+ try:
+ oso = get_oso_client()
+
+ # If user was previously an admin, remove global access
+ if old_role == "admin" and user.role != "admin":
+ remove_admin_global_access(user)
+
+ # If user became an admin, add global access
+ if user.role == "admin" and old_role != "admin":
+ sync_admin_global_access(user)
+
+ # Resync all patient access (for department nurse changes)
+ db = SessionLocal()
+ try:
+ patients = db.query(Patient).filter(Patient.is_active).all()
+ for patient in patients:
+ # Remove old nurse access for this user
+ user_value = Value("User", str(user.id))
+ patient_value = Value("Patient", str(patient.id))
+
+ oso.delete(("has_role", user_value, "department_nurse", patient_value))
+
+ # Add new nurse access if applicable
+ if user.role == "nurse" and patient.department == user.department:
+ oso.insert(("has_role", user_value, "department_nurse", patient_value))
+ finally:
+ db.close()
+
+ logger.info(f"Synced role change for user {user.id}: {old_role} -> {user.role}")
+ return True
+
+ except Exception as e:
+ logger.error(f"Failed to sync role change for user {user.id}: {e}")
+ return False
+
+
+def sync_department_change(user: User, old_department: str | None = None) -> bool:
+ """
+ Handle user department changes - update department-based access.
+ """
+ try:
+ oso = get_oso_client()
+ db = SessionLocal()
+
+ try:
+ user_value = Value("User", str(user.id))
+
+ # Remove old department nurse access
+ if old_department and user.role == "nurse":
+ old_patients = db.query(Patient).filter(Patient.department == old_department, Patient.is_active).all()
+
+ with oso.batch() as tx:
+ for patient in old_patients:
+ patient_value = Value("Patient", str(patient.id))
+ tx.delete(("has_role", user_value, "department_nurse", patient_value))
+
+ # Add new department nurse access
+ if user.department and user.role == "nurse":
+ new_patients = db.query(Patient).filter(Patient.department == user.department, Patient.is_active).all()
+
+ with oso.batch() as tx:
+ for patient in new_patients:
+ patient_value = Value("Patient", str(patient.id))
+ tx.insert(("has_role", user_value, "department_nurse", patient_value))
+
+ finally:
+ db.close()
+
+ logger.info(f"Synced department change for user {user.id}: {old_department} -> {user.department}")
+ return True
+
+ except Exception as e:
+ logger.error(f"Failed to sync department change for user {user.id}: {e}")
+ return False
+
+
+def full_resync() -> bool:
+ """
+ Perform a complete resynchronization of all facts.
+ Useful for initial setup or recovery from sync issues.
+ """
+ try:
+ db = SessionLocal()
+
+ try:
+ logger.info("Starting full fact resynchronization...")
+
+ # Clear all existing facts (be very careful with this!)
+ # oso.delete(("has_role", None, None, None)) # Uncomment if needed
+
+ # Sync all users
+ users = db.query(User).filter(User.is_active).all()
+ for user in users:
+ if user.role == "admin":
+ sync_admin_global_access(user)
+
+ # Sync all patients
+ patients = db.query(Patient).filter(Patient.is_active).all()
+ for patient in patients:
+ sync_patient_access(patient)
+
+ # Sync all documents
+ documents = db.query(Document).all()
+ for document in documents:
+ sync_document_access(document)
+
+ # Sync all embeddings
+ embeddings = db.query(Embedding).all()
+ for embedding in embeddings:
+ sync_embedding_access(embedding)
+
+ logger.info("Full fact resynchronization completed successfully")
+ return True
+
+ finally:
+ db.close()
+
+ except Exception as e:
+ logger.error(f"Failed during full resynchronization: {e}")
+ return False
diff --git a/python/rag/healthcare-support-portal/packages/common/src/common/schemas.py b/python/rag/healthcare-support-portal/packages/common/src/common/schemas.py
new file mode 100644
index 00000000..36cc2318
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/src/common/schemas.py
@@ -0,0 +1,77 @@
+from datetime import datetime
+
+from pydantic import BaseModel, ConfigDict
+
+
+# User schemas
+class UserBase(BaseModel):
+ username: str
+ email: str
+ role: str
+ department: str | None = None
+
+
+class UserCreate(UserBase):
+ password: str
+
+
+class UserResponse(UserBase):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: int
+ is_active: bool
+ created_at: datetime
+
+
+# Patient schemas
+class PatientBase(BaseModel):
+ name: str
+ medical_record_number: str
+ department: str | None = None
+
+
+class PatientCreate(PatientBase):
+ date_of_birth: str | None = None # ISO date string format YYYY-MM-DD
+ assigned_doctor_id: int | None = None
+
+
+class PatientResponse(PatientBase):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: int
+ date_of_birth: datetime | None = None
+ assigned_doctor_id: int | None
+ is_active: bool
+ created_at: datetime
+
+
+# Document schemas
+class DocumentBase(BaseModel):
+ title: str
+ content: str
+ document_type: str
+ department: str | None = None
+ is_sensitive: bool = False
+
+
+class DocumentCreate(DocumentBase):
+ patient_id: int | None = None
+
+
+class DocumentResponse(DocumentBase):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: int
+ patient_id: int | None
+ created_by_id: int
+ created_at: datetime
+
+
+# Token schemas
+class Token(BaseModel):
+ access_token: str
+ token_type: str
+
+
+class TokenData(BaseModel):
+ username: str | None = None
diff --git a/python/rag/healthcare-support-portal/packages/common/src/common/seed_data.py b/python/rag/healthcare-support-portal/packages/common/src/common/seed_data.py
new file mode 100644
index 00000000..109c4bf8
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/src/common/seed_data.py
@@ -0,0 +1,560 @@
+#!/usr/bin/env python3
+"""
+Database seeding script for Healthcare Support Portal
+Creates demo users, patients, and documents for development and demonstration.
+
+This script is idempotent - safe to run multiple times without creating duplicates.
+"""
+
+import sys
+import time
+
+import requests
+from sqlalchemy.orm import Session
+
+from .auth import get_password_hash
+from .db import SessionLocal
+from .models import Document, Patient, User
+from .oso_sync import (
+ sync_admin_global_access,
+ sync_patient_access,
+)
+
+# Service URLs - update these based on your environment
+AUTH_SERVICE_URL = "http://localhost:8001"
+PATIENT_SERVICE_URL = "http://localhost:8002"
+RAG_SERVICE_URL = "http://localhost:8003"
+
+
+def wait_for_services():
+ """Wait for services to be available before seeding"""
+ services = [
+ (AUTH_SERVICE_URL, "Auth Service"),
+ (PATIENT_SERVICE_URL, "Patient Service"),
+ (RAG_SERVICE_URL, "RAG Service"),
+ ]
+
+ for url, name in services:
+ for attempt in range(30): # Wait up to 30 seconds
+ try:
+ response = requests.get(f"{url}/health", timeout=2)
+ if response.status_code == 200:
+ print(f"✅ {name} is ready")
+ break
+ except requests.RequestException:
+ if attempt < 29:
+ print(f"⏳ Waiting for {name}... (attempt {attempt + 1})")
+ time.sleep(1)
+ else:
+ print(f"❌ {name} is not available after 30 seconds")
+ return False
+ return True
+
+
+def get_admin_token(db: Session) -> str:
+ """Get admin token for API calls"""
+ # First, create admin user directly in database (bootstrap)
+ admin_user = db.query(User).filter(User.username == "admin_wilson").first()
+ if not admin_user:
+ # Create admin user directly for bootstrapping
+ hashed_password = get_password_hash("secure_password")
+ admin_user = User(
+ username="admin_wilson",
+ email="jennifer.wilson@hospital.com",
+ hashed_password=hashed_password,
+ role="admin",
+ department="administration",
+ is_active=True,
+ )
+ db.add(admin_user)
+ db.commit()
+ db.refresh(admin_user)
+
+ # Sync OSO facts for admin user
+ try:
+ sync_admin_global_access(admin_user)
+ print(f"🔐 Created bootstrap admin user: {admin_user.username}")
+ except Exception as e:
+ print(f"⚠️ Failed to sync OSO facts for bootstrap admin: {e}")
+
+ # Get auth token
+ try:
+ response = requests.post(
+ f"{AUTH_SERVICE_URL}/api/v1/auth/login",
+ data={"username": "admin_wilson", "password": "secure_password"},
+ )
+ response.raise_for_status()
+ token_data = response.json()
+ return token_data["access_token"]
+ except Exception as e:
+ print(f"❌ Failed to get admin token: {e}")
+ return None
+
+
+def seed_users(db: Session, admin_token: str) -> dict[str, User]:
+ """Create demo users using API calls."""
+ created_users = {}
+
+ # Admin user is already created during bootstrap, just get it
+ admin_user = db.query(User).filter(User.username == "admin_wilson").first()
+ if admin_user:
+ created_users["admin_wilson"] = admin_user
+ print("✅ Using bootstrap admin user: admin_wilson")
+
+ demo_users = [
+ {
+ "username": "dr_smith",
+ "email": "sarah.smith@hospital.com",
+ "password": "secure_password",
+ "role": "doctor",
+ "department": "cardiology",
+ "full_name": "Dr. Sarah Smith",
+ },
+ {
+ "username": "nurse_johnson",
+ "email": "michael.johnson@hospital.com",
+ "password": "secure_password",
+ "role": "nurse",
+ "department": "emergency",
+ "full_name": "Nurse Michael Johnson",
+ },
+ ]
+
+ headers = {"Authorization": f"Bearer {admin_token}"}
+
+ for user_data in demo_users:
+ # Check if user already exists
+ existing_user = db.query(User).filter(User.username == user_data["username"]).first()
+ if existing_user:
+ print(f"✅ User '{user_data['username']}' already exists, skipping")
+ created_users[user_data["username"]] = existing_user
+ continue
+
+ # Create user via API
+ try:
+ response = requests.post(
+ f"{AUTH_SERVICE_URL}/api/v1/users/",
+ json={
+ "username": user_data["username"],
+ "email": user_data["email"],
+ "password": user_data["password"],
+ "role": user_data["role"],
+ "department": user_data["department"],
+ },
+ headers=headers,
+ )
+
+ if response.status_code == 200:
+ user_response = response.json()
+ # Get user from database
+ new_user = db.query(User).filter(User.id == user_response["id"]).first()
+ created_users[user_data["username"]] = new_user
+ print(f"✅ Created user via API: {user_data['full_name']} ({user_data['username']})")
+ else:
+ print(f"⚠️ Failed to create user {user_data['username']}: {response.text}")
+
+ except Exception as e:
+ print(f"⚠️ Error creating user {user_data['username']}: {e}")
+
+ return created_users
+
+
+def seed_patients(db: Session, users: dict[str, User], admin_token: str) -> list[Patient]:
+ """Create demo patients via API if they don't exist."""
+ created_patients = []
+
+ # Get the doctor user
+ doctor = users.get("dr_smith")
+ if not doctor:
+ print("⚠️ Doctor user not found, skipping patient creation")
+ return created_patients
+
+ demo_patients = [
+ {
+ "name": "John Anderson",
+ "medical_record_number": "MRN-2024-001",
+ "department": "cardiology",
+ "date_of_birth": "1965-03-15",
+ "assigned_doctor_id": doctor.id,
+ },
+ {
+ "name": "Maria Rodriguez",
+ "medical_record_number": "MRN-2024-002",
+ "department": "cardiology",
+ "date_of_birth": "1978-08-22",
+ "assigned_doctor_id": doctor.id,
+ },
+ {
+ "name": "Robert Chen",
+ "medical_record_number": "MRN-2024-003",
+ "department": "cardiology",
+ "date_of_birth": "1952-11-08",
+ "assigned_doctor_id": doctor.id,
+ },
+ ]
+
+ headers = {"Authorization": f"Bearer {admin_token}"}
+
+ for patient_data in demo_patients:
+ # Check if patient already exists
+ existing_patient = db.query(Patient).filter(Patient.medical_record_number == patient_data["medical_record_number"]).first()
+ if existing_patient:
+ print(f"✅ Patient '{patient_data['name']}' (MRN: {patient_data['medical_record_number']}) already exists, skipping")
+ created_patients.append(existing_patient)
+
+ # Sync OSO facts for existing patients (in case they weren't synced before)
+ try:
+ sync_patient_access(existing_patient)
+ print(f"🔐 Synced access facts for existing patient {patient_data['name']}")
+ except Exception as e:
+ print(f"⚠️ Failed to sync OSO facts for existing patient {patient_data['name']}: {e}")
+ continue
+
+ # Create patient via Patient Service API
+ try:
+ response = requests.post(
+ f"{PATIENT_SERVICE_URL}/api/v1/patients/",
+ json={
+ "name": patient_data["name"],
+ "medical_record_number": patient_data["medical_record_number"],
+ "department": patient_data["department"],
+ "date_of_birth": patient_data["date_of_birth"],
+ "assigned_doctor_id": patient_data["assigned_doctor_id"],
+ },
+ headers=headers,
+ )
+
+ if response.status_code == 200:
+ patient_response = response.json()
+ # Get patient from database
+ new_patient = db.query(Patient).filter(Patient.id == patient_response["id"]).first()
+ created_patients.append(new_patient)
+ print(f"✅ Created patient via API (with OSO facts): {patient_data['name']} (MRN: {patient_data['medical_record_number']})")
+ else:
+ print(f"⚠️ Failed to create patient {patient_data['name']}: {response.text}")
+
+ except Exception as e:
+ print(f"⚠️ Error creating patient {patient_data['name']}: {e}")
+
+ return created_patients
+
+
+def seed_documents(db: Session, users: dict[str, User], patients: list[Patient], admin_token: str) -> list[Document]:
+ """Create demo documents if they don't exist."""
+ created_documents = []
+
+ # Get the doctor user
+ doctor = users.get("dr_smith")
+ if not doctor:
+ print("⚠️ Doctor user not found, skipping document creation")
+ return created_documents
+
+ demo_documents = [
+ {
+ "title": "Cardiology Department Protocols",
+ "content": """# Cardiology Department Standard Operating Procedures
+
+## Patient Assessment Protocols
+1. Initial cardiac risk assessment for all new patients
+2. ECG interpretation guidelines and red flags
+3. Chest pain evaluation protocols
+4. Heart failure management guidelines
+
+## Diagnostic Procedures
+- Echocardiogram ordering criteria
+- Stress test indications and contraindications
+- Cardiac catheterization preparation protocols
+- Post-procedure monitoring requirements
+
+## Medication Management
+- ACE inhibitor dosing and monitoring
+- Beta-blocker titration protocols
+- Anticoagulation management
+- Drug interaction guidelines
+
+## Emergency Procedures
+- Code Blue response protocols
+- Cardiac arrest management
+- Acute MI treatment pathways
+- Arrhythmia management protocols
+
+Last updated: January 2024""",
+ "document_type": "protocol",
+ "department": "cardiology",
+ "is_sensitive": False,
+ },
+ {
+ "title": "Hypertension Management Guidelines",
+ "content": """# Hypertension Management Guidelines
+
+## Classification
+- Normal: <120/80 mmHg
+- Elevated: 120-129/<80 mmHg
+- Stage 1: 130-139/80-89 mmHg
+- Stage 2: ≥140/≥90 mmHg
+- Crisis: >180/>120 mmHg
+
+## Initial Assessment
+1. Confirm diagnosis with multiple readings
+2. Assess for secondary causes
+3. Evaluate cardiovascular risk factors
+4. Screen for target organ damage
+
+## Treatment Approach
+### Lifestyle Modifications
+- DASH diet implementation
+- Sodium restriction (<2.3g/day)
+- Weight management (BMI <25)
+- Regular aerobic exercise (150 min/week)
+- Alcohol moderation
+- Smoking cessation
+
+### Pharmacological Treatment
+First-line agents:
+- ACE inhibitors or ARBs
+- Calcium channel blockers
+- Thiazide diuretics
+
+## Monitoring
+- BP checks every 2-4 weeks during titration
+- Annual lab work (creatinine, potassium)
+- Yearly cardiovascular risk assessment
+
+Target: <130/80 mmHg for most patients""",
+ "document_type": "guideline",
+ "department": "cardiology",
+ "is_sensitive": False,
+ },
+ {
+ "title": "Heart Failure Patient Education",
+ "content": """# Heart Failure: Patient Education Guide
+
+## What is Heart Failure?
+Heart failure occurs when your heart cannot pump blood effectively to meet your body's needs. This doesn't mean your heart has stopped working, but rather that it needs support to work better.
+
+## Common Symptoms
+- Shortness of breath (especially when lying down)
+- Fatigue and weakness
+- Swelling in legs, ankles, or feet
+- Rapid or irregular heartbeat
+- Persistent cough or wheezing
+- Weight gain from fluid retention
+
+## Daily Management
+### Medications
+- Take all medications as prescribed
+- Never skip doses
+- Know your medications and their purposes
+- Report side effects to your healthcare team
+
+### Diet and Fluid Management
+- Limit sodium intake (less than 2 grams daily)
+- Monitor fluid intake as directed
+- Weigh yourself daily at the same time
+- Call if weight increases by 2-3 pounds in one day
+
+### Activity Guidelines
+- Stay active within your limits
+- Pace yourself and rest when needed
+- Avoid sudden increases in activity
+- Participate in cardiac rehabilitation if recommended
+
+## When to Call Your Doctor
+- Weight gain of 2-3 pounds in one day
+- Increased shortness of breath
+- New or worsening swelling
+- Chest pain or discomfort
+- Dizziness or fainting
+- Any concerns about your condition
+
+Remember: You are an important part of your healthcare team!""",
+ "document_type": "education",
+ "department": "cardiology",
+ "is_sensitive": False,
+ },
+ ]
+
+ headers = {"Authorization": f"Bearer {admin_token}"}
+
+ for doc_data in demo_documents:
+ # Check if document already exists (by title)
+ existing_doc = db.query(Document).filter(Document.title == doc_data["title"]).first()
+ if existing_doc:
+ print(f"✅ Document '{doc_data['title']}' already exists, skipping")
+ created_documents.append(existing_doc)
+ continue
+
+ # Create document via RAG service API (this will automatically generate embeddings!)
+ try:
+ response = requests.post(
+ f"{RAG_SERVICE_URL}/api/v1/documents/",
+ json={
+ "title": doc_data["title"],
+ "content": doc_data["content"],
+ "document_type": doc_data["document_type"],
+ "department": doc_data["department"],
+ "is_sensitive": doc_data["is_sensitive"],
+ },
+ headers=headers,
+ )
+
+ if response.status_code == 200:
+ doc_response = response.json()
+ # Get document from database
+ new_document = db.query(Document).filter(Document.id == doc_response["id"]).first()
+ created_documents.append(new_document)
+ print(f"✅ Created document via RAG API (with embeddings): {doc_data['title']}")
+ else:
+ print(f"⚠️ Failed to create document {doc_data['title']}: {response.text}")
+
+ except Exception as e:
+ print(f"⚠️ Error creating document {doc_data['title']}: {e}")
+
+ # Create patient-specific documents
+ if patients:
+ patient_docs = [
+ {
+ "title": f"Medical History - {patients[0].name}",
+ "content": f"""# Medical History for {patients[0].name}
+MRN: {patients[0].medical_record_number}
+
+## Chief Complaint
+Routine cardiac follow-up for hypertension and coronary artery disease.
+
+## History of Present Illness
+67-year-old male with history of hypertension, hyperlipidemia, and prior MI in 2020. Currently stable on optimal medical therapy. Reports good exercise tolerance and no chest pain.
+
+## Past Medical History
+- Hypertension (2015)
+- Hyperlipidemia (2016)
+- ST-elevation myocardial infarction (2020)
+- Percutaneous coronary intervention with drug-eluting stent to LAD (2020)
+
+## Current Medications
+- Metoprolol succinate 50mg daily
+- Lisinopril 10mg daily
+- Atorvastatin 80mg daily
+- Aspirin 81mg daily
+- Clopidogrel 75mg daily
+
+## Allergies
+No known drug allergies
+
+## Social History
+Former smoker (quit 2020), occasional alcohol use, married, retired electrician.
+
+## Assessment and Plan
+Stable coronary artery disease. Continue current medications. Next follow-up in 6 months with stress test if symptoms develop.""",
+ "document_type": "medical_record",
+ "patient_id": patients[0].id,
+ "is_sensitive": True,
+ }
+ ]
+
+ for doc_data in patient_docs:
+ # Check if patient document already exists
+ existing_doc = (
+ db.query(Document)
+ .filter(
+ Document.title == doc_data["title"],
+ Document.patient_id == doc_data["patient_id"],
+ )
+ .first()
+ )
+ if existing_doc:
+ print(f"✅ Patient document '{doc_data['title']}' already exists, skipping")
+ created_documents.append(existing_doc)
+ continue
+
+ # Create patient document via RAG service API (with embeddings!)
+ try:
+ response = requests.post(
+ f"{RAG_SERVICE_URL}/api/v1/documents/",
+ json={
+ "title": doc_data["title"],
+ "content": doc_data["content"],
+ "document_type": doc_data["document_type"],
+ "department": "cardiology",
+ "patient_id": doc_data["patient_id"],
+ "is_sensitive": doc_data["is_sensitive"],
+ },
+ headers=headers,
+ )
+
+ if response.status_code == 200:
+ doc_response = response.json()
+ # Get document from database
+ new_document = db.query(Document).filter(Document.id == doc_response["id"]).first()
+ created_documents.append(new_document)
+ print(f"✅ Created patient document via RAG API (with embeddings): {doc_data['title']}")
+ else:
+ print(f"⚠️ Failed to create patient document {doc_data['title']}: {response.text}")
+
+ except Exception as e:
+ print(f"⚠️ Error creating patient document {doc_data['title']}: {e}")
+
+ return created_documents
+
+
+def main() -> None:
+ """Main seeding function."""
+ print("🌱 Healthcare Support Portal - Database Seeding")
+ print("=" * 50)
+
+ try:
+ # Note: Database migrations should be run before seeding
+ print("📊 Database migrations should already be applied")
+ print(" If not, run: docker-compose run migrate")
+
+ # Create database session
+ db = SessionLocal()
+
+ try:
+ # Wait for services to be available
+ print("\n⏳ Waiting for services to be ready...")
+ if not wait_for_services():
+ print("❌ Services not available, exiting...")
+ return
+
+ # Get admin token for API calls
+ print("\n🔑 Getting admin authentication token...")
+ admin_token = get_admin_token(db)
+ if not admin_token:
+ print("❌ Could not get admin token, exiting...")
+ return
+
+ # Seed users via API
+ print("\n👥 Seeding demo users via API...")
+ users = seed_users(db, admin_token)
+
+ # Seed patients via Patient API
+ print("\n🏥 Seeding demo patients via API...")
+ patients = seed_patients(db, users, admin_token)
+
+ # Seed documents via RAG API (this will generate embeddings!)
+ print("\n📄 Seeding demo documents via RAG API...")
+ documents = seed_documents(db, users, patients, admin_token)
+
+ print("\n" + "=" * 50)
+ print("🎉 Seeding completed successfully!")
+ print(f" Users created/verified: {len(users)}")
+ print(f" Patients created/verified: {len(patients)}")
+ print(f" Documents created/verified: {len(documents)}")
+ print(" 📊 Documents created via RAG API include vector embeddings for chat!")
+ print("\n🔐 Demo Login Credentials:")
+ print(" Doctor: dr_smith / secure_password")
+ print(" Nurse: nurse_johnson / secure_password")
+ print(" Admin: admin_wilson / secure_password")
+ print("\n🌐 Access the application at: http://localhost:3000")
+ print("💬 Try the chat feature - it should now find relevant documents!")
+
+ finally:
+ db.close()
+
+ except Exception as e:
+ print(f"\n❌ Seeding failed: {str(e)}")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/python/rag/healthcare-support-portal/packages/common/src/common/sync_oso_facts.py b/python/rag/healthcare-support-portal/packages/common/src/common/sync_oso_facts.py
new file mode 100644
index 00000000..4e7075ab
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/common/src/common/sync_oso_facts.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+"""
+Standalone script to synchronize OSO facts with database state.
+This script initializes sqlalchemy-oso-cloud and syncs authorization facts.
+"""
+
+import os
+import sys
+
+import sqlalchemy_oso_cloud
+from sqlalchemy.orm import Session
+
+from .db import SessionLocal
+from .models import Base, Document, Embedding, Patient, User
+from .oso_sync import (
+ sync_admin_global_access,
+ sync_document_access,
+ sync_embedding_access,
+ sync_patient_access,
+)
+
+
+def initialize_oso() -> None:
+ """Initialize SQLAlchemy OSO Cloud connection."""
+ # OSO configuration - same as in the services
+ oso_url = os.getenv("OSO_URL", "http://localhost:8080")
+ oso_auth = os.getenv("OSO_AUTH", "e_0123456789_12345_osotesttoken01xiIn")
+
+ # Initialize SQLAlchemy Oso Cloud with registry and server settings
+ sqlalchemy_oso_cloud.init(Base.registry, url=oso_url, api_key=oso_auth)
+
+ print(f"✅ Initialized OSO Cloud connection: {oso_url}")
+
+
+def sync_all_facts() -> None:
+ """Synchronize all authorization facts with OSO Cloud."""
+ db: Session = SessionLocal()
+
+ try:
+ print("🔐 Starting comprehensive OSO fact synchronization...")
+
+ # Sync admin users first
+ print("\n👑 Syncing admin user facts...")
+ admin_users = db.query(User).filter(User.role == "admin", User.is_active).all()
+ for admin in admin_users:
+ try:
+ sync_admin_global_access(admin)
+ print(f"✅ Synced global admin access for {admin.username}")
+ except Exception as e:
+ print(f"❌ Failed to sync admin access for {admin.username}: {e}")
+
+ # Sync all patients
+ print("\n🏥 Syncing patient facts...")
+ patients = db.query(Patient).filter(Patient.is_active).all()
+ for patient in patients:
+ try:
+ sync_patient_access(patient)
+ print(f"✅ Synced patient access for {patient.name}")
+ except Exception as e:
+ print(f"❌ Failed to sync patient access for {patient.name}: {e}")
+
+ # Sync all documents
+ print("\n📄 Syncing document facts...")
+ documents = db.query(Document).all()
+ for document in documents:
+ try:
+ sync_document_access(document)
+ print(f"✅ Synced document access for '{document.title}'")
+ except Exception as e:
+ print(f"❌ Failed to sync document access for '{document.title}': {e}")
+
+ # Sync all embeddings
+ print("\n🧮 Syncing embedding facts...")
+ embeddings = db.query(Embedding).all()
+ for embedding in embeddings:
+ try:
+ sync_embedding_access(embedding)
+ print(f"✅ Synced embedding access for embedding {embedding.id}")
+ except Exception as e:
+ print(f"❌ Failed to sync embedding access for embedding {embedding.id}: {e}")
+
+ print("\n🎉 OSO fact synchronization completed!")
+ print(f" Admin users: {len(admin_users)}")
+ print(f" Patients: {len(patients)}")
+ print(f" Documents: {len(documents)}")
+ print(f" Embeddings: {len(embeddings)}")
+
+ finally:
+ db.close()
+
+
+def main() -> None:
+ """Main function."""
+ print("🔐 OSO Cloud Fact Synchronization")
+ print("=" * 50)
+
+ try:
+ # Initialize OSO Cloud connection
+ initialize_oso()
+
+ # Synchronize all facts
+ sync_all_facts()
+
+ print("\n✅ All facts synchronized successfully!")
+
+ except Exception as e:
+ print(f"\n❌ Fact synchronization failed: {str(e)}")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/python/rag/healthcare-support-portal/packages/patient/.env.example b/python/rag/healthcare-support-portal/packages/patient/.env.example
new file mode 100644
index 00000000..4d8fad67
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/patient/.env.example
@@ -0,0 +1,6 @@
+# Patient Service Environment Variables
+DEBUG=true
+SECRET_KEY=change-me-in-production
+DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare
+OSO_URL=http://localhost:8080
+OSO_AUTH=e_0123456789_12345_osotesttoken01xiIn
diff --git a/python/rag/healthcare-support-portal/packages/patient/.gitignore b/python/rag/healthcare-support-portal/packages/patient/.gitignore
new file mode 100644
index 00000000..cb43b4dc
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/patient/.gitignore
@@ -0,0 +1,7 @@
+# Patient Service Specific
+.env
+*.log
+logs/
+.pytest_cache/
+__pycache__/
+*.pyc
diff --git a/python/rag/healthcare-support-portal/packages/patient/README.md b/python/rag/healthcare-support-portal/packages/patient/README.md
new file mode 100644
index 00000000..e7e8292c
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/patient/README.md
@@ -0,0 +1,299 @@
+# Patient Service
+
+The Patient Service manages patient records and information for the Healthcare Support Portal. It provides CRUD operations with comprehensive role-based authorization using Oso policies.
+
+## Features
+
+- **Patient CRUD:** Create, read, update, and delete patient records
+- **Authorization:** Role-based access control with Oso policies
+- **Department Filtering:** Search patients by department
+- **Doctor Assignment:** Assign and manage doctor-patient relationships
+- **Soft Delete:** Deactivate patients instead of hard deletion
+- **Pagination:** Efficient handling of large patient lists
+- **Medical Record Validation:** Ensure unique medical record numbers
+
+## Quick Start
+
+### Prerequisites
+
+- Python 3.8+
+- PostgreSQL with the Healthcare Support Portal database
+- uv package manager
+- Authentication Service running (for JWT validation)
+
+### Installation
+
+```bash
+# From project root
+uv sync
+
+# Or from package directory
+cd packages/patient
+uv sync
+```
+
+### Environment Variables
+
+Create a `.env` file or set these environment variables:
+
+```env
+DEBUG=true
+SECRET_KEY=your-secret-key-here
+DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare
+```
+
+### Running the Service
+
+```bash
+# Set PYTHONPATH and run
+export PYTHONPATH="../common/src:$PYTHONPATH"
+uv run uvicorn src.patient_service.main:app --reload --port 8002
+
+# Or use the run script from package directory
+cd packages/patient
+./run.sh
+```
+
+The service will be available at http://localhost:8002
+
+### API Documentation
+
+Interactive API docs are available at:
+- Swagger UI: http://localhost:8002/docs
+- ReDoc: http://localhost:8002/redoc
+
+## API Endpoints
+
+### Patient Management
+
+| Method | Endpoint | Description | Auth Required | Roles |
+|--------|----------|-------------|---------------|-------|
+| GET | `/api/v1/patients/` | List patients (authorized) | Yes | doctor, nurse, admin |
+| GET | `/api/v1/patients/{patient_id}` | Get specific patient | Yes | assigned doctor, dept nurse, admin |
+| POST | `/api/v1/patients/` | Create new patient | Yes | doctor, admin |
+| PUT | `/api/v1/patients/{patient_id}` | Update patient | Yes | assigned doctor, admin |
+| DELETE | `/api/v1/patients/{patient_id}` | Soft delete patient | Yes | assigned doctor, admin |
+| GET | `/api/v1/patients/search/by-department/{dept}` | Search by department | Yes | dept nurse, admin |
+
+### Health Check
+
+| Method | Endpoint | Description | Auth Required |
+|--------|----------|-------------|---------------|
+| GET | `/health` | Service health check | No |
+| GET | `/` | Service info | No |
+
+## Example Usage
+
+### Get Your JWT Token First
+
+```bash
+# Login via auth service
+curl -X POST "http://localhost:8001/api/v1/auth/login" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "username=dr_smith&password=secure_password"
+```
+
+### Create a New Patient
+
+```bash
+curl -X POST "http://localhost:8002/api/v1/patients/" \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "John Doe",
+ "medical_record_number": "MRN-2024-001",
+ "department": "cardiology",
+ "assigned_doctor_id": 1
+ }'
+```
+
+### List Patients (with pagination)
+
+```bash
+curl -X GET "http://localhost:8002/api/v1/patients/?skip=0&limit=10" \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN"
+```
+
+### Get Specific Patient
+
+```bash
+curl -X GET "http://localhost:8002/api/v1/patients/1" \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN"
+```
+
+### Update Patient
+
+```bash
+curl -X PUT "http://localhost:8002/api/v1/patients/1" \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "John Doe",
+ "medical_record_number": "MRN-2024-001",
+ "department": "cardiology",
+ "assigned_doctor_id": 2
+ }'
+```
+
+### Search Patients by Department
+
+```bash
+curl -X GET "http://localhost:8002/api/v1/patients/search/by-department/cardiology?skip=0&limit=10" \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN"
+```
+
+## Authorization Rules
+
+The service implements comprehensive authorization using Oso policies:
+
+### Doctor Access
+- **Read:** Only assigned patients
+- **Write:** Only assigned patients
+- **Create:** Can create new patients
+
+### Nurse Access
+- **Read:** Patients in their department (non-sensitive)
+- **Write:** No write access
+- **Create:** No create access
+
+### Admin Access
+- **Read:** All patients
+- **Write:** All patients
+- **Create:** Can create new patients
+
+## Data Models
+
+### Patient Schema
+
+```json
+{
+ "id": 1,
+ "name": "John Doe",
+ "medical_record_number": "MRN-2024-001",
+ "department": "cardiology",
+ "assigned_doctor_id": 1,
+ "is_active": true,
+ "created_at": "2024-01-15T10:00:00Z"
+}
+```
+
+### Required Fields
+- `name`: Patient's full name
+- `medical_record_number`: Unique identifier (must be unique)
+
+### Optional Fields
+- `department`: Medical department
+- `assigned_doctor_id`: ID of assigned doctor
+- `date_of_birth`: Patient's birth date
+
+## Query Parameters
+
+### Pagination
+- `skip`: Number of records to skip (default: 0)
+- `limit`: Maximum records to return (default: 100, max: 1000)
+
+### Filtering
+- `department`: Filter by department name
+
+## Development
+
+### Project Structure
+
+```
+src/patient_service/
+├── __init__.py
+├── main.py # FastAPI application
+├── config.py # Configuration settings
+└── routers/
+ ├── __init__.py
+ └── patients.py # Patient management endpoints
+```
+
+### Dependencies
+
+Key dependencies include:
+- FastAPI: Web framework
+- SQLAlchemy: ORM for database operations
+- Oso: Authorization framework
+- sqlalchemy-oso: SQLAlchemy integration for Oso
+- common: Shared models and utilities
+
+### Testing
+
+```bash
+# Test imports
+uv run python -c "from common.models import Patient; print('Import successful!')"
+
+# Test database connection
+uv run python -c "from common.db import create_tables; create_tables(); print('DB connection successful!')"
+```
+
+## Error Handling
+
+The service returns standard HTTP status codes:
+
+- `200`: Success
+- `201`: Created
+- `400`: Bad Request (validation errors)
+- `401`: Unauthorized (invalid/missing token)
+- `403`: Forbidden (insufficient permissions)
+- `404`: Not Found (patient doesn't exist)
+- `422`: Unprocessable Entity (invalid data)
+- `500`: Internal Server Error
+
+### Common Error Responses
+
+```json
+{
+ "detail": "Not authorized to access this patient"
+}
+```
+
+```json
+{
+ "detail": "Patient with this medical record number already exists"
+}
+```
+
+## Performance Considerations
+
+- Pagination is enforced to prevent large result sets
+- Database indexes on frequently queried fields
+- Oso authorization queries are optimized at the database level
+- Soft delete preserves data integrity
+
+## Security Features
+
+- JWT token validation on all protected endpoints
+- Role-based authorization with Oso policies
+- Input validation and sanitization
+- Medical record number uniqueness enforcement
+- Audit trail through created_at timestamps
+
+## Integration
+
+This service integrates with:
+- **Auth Service:** JWT token validation and user information
+- **RAG Service:** Patient-specific document access
+- **Common Package:** Shared models, database, and utilities
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Authorization errors:** Ensure user has correct role and department
+2. **Database errors:** Check PostgreSQL connection and table creation
+3. **Import errors:** Verify PYTHONPATH includes `../common/src`
+4. **Token errors:** Ensure auth service is running and SECRET_KEY matches
+
+### Debug Mode
+
+Set `DEBUG=true` for detailed error messages and SQL query logging.
+
+## Contributing
+
+1. Follow existing code patterns and structure
+2. Add appropriate authorization checks for new endpoints
+3. Update Oso policies for new access patterns
+4. Include comprehensive error handling
+5. Update this README for new features
diff --git a/python/rag/healthcare-support-portal/packages/patient/pyproject.toml b/python/rag/healthcare-support-portal/packages/patient/pyproject.toml
new file mode 100644
index 00000000..6e881b0c
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/patient/pyproject.toml
@@ -0,0 +1,25 @@
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "patient"
+version = "0.1.0"
+description = "Patient management service for Healthcare Support Portal"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+ "fastapi>=0.104.0",
+ "uvicorn[standard]>=0.24.0",
+ "pydantic>=2.0.0",
+ "pydantic-settings>=2.0.0",
+ "sqlalchemy>=2.0.0",
+ "common",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/patient_service"]
+
+[tool.uv.sources]
+common = { workspace = true }
diff --git a/python/rag/healthcare-support-portal/packages/patient/run.sh b/python/rag/healthcare-support-portal/packages/patient/run.sh
new file mode 100755
index 00000000..5e49e501
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/patient/run.sh
@@ -0,0 +1,67 @@
+#!/bin/bash
+# packages/patient/run.sh
+
+# Exit on any error
+set -e
+
+# Function to check if a command exists
+command_exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+# Load .env file if it exists
+if [ -f .env ]; then
+ echo "Loading environment variables from .env file..."
+ set -o allexport
+ source .env
+ set +o allexport
+else
+ echo "⚠️ Warning: No .env file found. Using defaults."
+fi
+
+# Set PYTHONPATH to include common package
+export PYTHONPATH="../common/src:$PYTHONPATH"
+
+# Set default environment variables if not already set
+export SECRET_KEY="${SECRET_KEY:-your-secret-key-here}"
+export DATABASE_URL="${DATABASE_URL:-postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare}"
+export DEBUG="${DEBUG:-true}"
+export HOST="${HOST:-0.0.0.0}"
+export PORT="${PORT:-8002}"
+
+echo "🏥 Starting Patient Service on port $PORT..."
+echo "📊 Debug mode: $DEBUG"
+echo "🔑 Using SECRET_KEY: ${SECRET_KEY:0:10}..."
+echo "🗄️ Database: ${DATABASE_URL%%@*}@***"
+
+# Check for uv command
+UV_CMD="uv"
+if [ -f "/opt/homebrew/bin/uv" ]; then
+ UV_CMD="/opt/homebrew/bin/uv"
+fi
+
+if ! command_exists "$UV_CMD"; then
+ echo "❌ uv package manager not found. Please install uv and try again."
+ echo " Installation: curl -LsSf https://astral.sh/uv/install.sh | sh"
+ exit 1
+fi
+
+# Check if port is available
+if command_exists lsof; then
+ if lsof -i :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
+ echo "❌ Port $PORT is already in use. Please stop the service using this port."
+ exit 1
+ fi
+fi
+
+# Check essential environment variables
+if [ "$SECRET_KEY" = "your-secret-key-here" ] || [ "$SECRET_KEY" = "change-me-in-production" ]; then
+ echo "⚠️ WARNING: Using default SECRET_KEY. This is insecure for production!"
+fi
+
+# Run the patient service
+echo "🚀 Starting uvicorn server..."
+if ! $UV_CMD run uvicorn src.patient_service.main:app --reload --host $HOST --port $PORT; then
+ echo "❌ Failed to start Patient Service"
+ exit 1
+fi
diff --git a/python/rag/healthcare-support-portal/packages/patient/src/patient_service/__init__.py b/python/rag/healthcare-support-portal/packages/patient/src/patient_service/__init__.py
new file mode 100644
index 00000000..7ed4e7de
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/patient/src/patient_service/__init__.py
@@ -0,0 +1,6 @@
+"""
+Patient Service for Healthcare Support Portal
+Handles patient management with role-based access control
+"""
+
+__version__ = "0.1.0"
diff --git a/python/rag/healthcare-support-portal/packages/patient/src/patient_service/config.py b/python/rag/healthcare-support-portal/packages/patient/src/patient_service/config.py
new file mode 100644
index 00000000..da3a3796
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/patient/src/patient_service/config.py
@@ -0,0 +1,29 @@
+import os
+
+from pydantic_settings import BaseSettings
+
+
+class Settings(BaseSettings):
+ app_name: str = "Healthcare Support Portal - Patient Service"
+ debug: bool = os.getenv("DEBUG", "False").lower() == "true"
+ host: str = "0.0.0.0"
+ port: int = 8002
+
+ # Database
+ database_url: str = os.getenv(
+ "DATABASE_URL",
+ "postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare",
+ )
+
+ # JWT
+ secret_key: str = os.getenv("SECRET_KEY", "your-secret-key-here")
+
+ # Oso Configuration
+ oso_url: str = os.getenv("OSO_URL", "http://localhost:8080")
+ oso_auth: str = os.getenv("OSO_AUTH", "e_0123456789_12345_osotesttoken01xiIn")
+
+ class Config:
+ env_file = ".env"
+
+
+settings = Settings()
diff --git a/python/rag/healthcare-support-portal/packages/patient/src/patient_service/main.py b/python/rag/healthcare-support-portal/packages/patient/src/patient_service/main.py
new file mode 100644
index 00000000..616bc9f5
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/patient/src/patient_service/main.py
@@ -0,0 +1,60 @@
+import sys
+from pathlib import Path
+
+# Add the common package to Python path
+common_path = Path(__file__).parent.parent.parent.parent / "common" / "src"
+sys.path.insert(0, str(common_path))
+
+import sqlalchemy_oso_cloud
+from common.migration_check import require_migrations_current
+from common.models import Base
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from .config import settings
+from .routers import patients
+
+# Initialize SQLAlchemy Oso Cloud with registry and server settings
+sqlalchemy_oso_cloud.init(Base.registry, url=settings.oso_url, api_key=settings.oso_auth)
+
+# Create FastAPI app
+app = FastAPI(
+ title=settings.app_name,
+ description="Patient management service with role-based access control",
+ version="0.1.0",
+ debug=settings.debug,
+)
+
+# Add CORS middleware
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # Configure appropriately for production
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Include routers
+app.include_router(patients.router, prefix="/api/v1/patients", tags=["Patients"])
+
+
+@app.on_event("startup")
+async def startup_event():
+ """Verify migrations and start service"""
+ # Verify database migrations are current
+ require_migrations_current()
+ print(f"🚀 {settings.app_name} started on port {settings.port}")
+
+
+@app.get("/")
+async def root():
+ return {"service": "patient_service", "status": "healthy", "version": "0.1.0"}
+
+
+@app.get("/health")
+async def health_check():
+ return {"status": "healthy"}
+
+
+# Make Oso Cloud instance available to routes
+app.state.oso_sqlalchemy = sqlalchemy_oso_cloud
diff --git a/python/rag/healthcare-support-portal/packages/patient/src/patient_service/routers/__init__.py b/python/rag/healthcare-support-portal/packages/patient/src/patient_service/routers/__init__.py
new file mode 100644
index 00000000..5bb3dc22
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/patient/src/patient_service/routers/__init__.py
@@ -0,0 +1 @@
+# Router package initialization
diff --git a/python/rag/healthcare-support-portal/packages/patient/src/patient_service/routers/patients.py b/python/rag/healthcare-support-portal/packages/patient/src/patient_service/routers/patients.py
new file mode 100644
index 00000000..f5668c96
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/patient/src/patient_service/routers/patients.py
@@ -0,0 +1,284 @@
+import sys
+from pathlib import Path
+
+# Add the common package to Python path
+common_path = Path(__file__).parent.parent.parent.parent.parent / "common" / "src"
+sys.path.insert(0, str(common_path))
+
+
+from common.auth import get_current_user
+from common.db import get_db
+from common.models import Patient, User
+from common.oso_sync import remove_patient_access, sync_patient_access
+from common.schemas import PatientCreate, PatientResponse
+from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
+from sqlalchemy.orm import Session
+from sqlalchemy_oso_cloud import authorized, get_oso
+
+router = APIRouter()
+
+
+@router.get("/", response_model=list[PatientResponse])
+async def list_patients(
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+ skip: int = Query(0, ge=0),
+ limit: int = Query(100, ge=1, le=1000),
+ department: str | None = Query(None),
+):
+ """
+ List patients with Oso authorization filtering
+ """
+ # Use Oso Cloud to filter patients the current user can read
+ query = db.query(Patient).options(authorized(current_user, "read", Patient))
+
+ # Apply optional department filter
+ if department:
+ query = query.filter(Patient.department == department)
+
+ # Apply pagination
+ patients = query.offset(skip).limit(limit).all()
+
+ return patients
+
+
+@router.get("/{patient_id}", response_model=PatientResponse)
+async def get_patient(
+ patient_id: int,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Get specific patient with Oso authorization
+ """
+ oso = get_oso()
+
+ # Get the patient
+ patient = db.query(Patient).filter(Patient.id == patient_id).first()
+ if not patient:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Patient not found")
+
+ # Check if current user is authorized to read this patient
+ if not oso.authorize(current_user, "read", patient):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Not authorized to access this patient",
+ )
+
+ return patient
+
+
+@router.post("/", response_model=PatientResponse)
+async def create_patient(
+ patient_data: PatientCreate,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Create a new patient
+ """
+ # Check if medical record number already exists
+ existing_patient = db.query(Patient).filter(Patient.medical_record_number == patient_data.medical_record_number).first()
+
+ if existing_patient:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Patient with this medical record number already exists",
+ )
+
+ # Create new patient
+ # Parse date_of_birth if provided
+ date_of_birth = None
+ if patient_data.date_of_birth:
+ try:
+ from datetime import datetime
+
+ date_of_birth = datetime.fromisoformat(patient_data.date_of_birth)
+ except ValueError:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Invalid date_of_birth format. Use YYYY-MM-DD",
+ )
+
+ db_patient = Patient(
+ name=patient_data.name,
+ medical_record_number=patient_data.medical_record_number,
+ department=patient_data.department,
+ date_of_birth=date_of_birth,
+ assigned_doctor_id=patient_data.assigned_doctor_id,
+ is_active=True,
+ )
+
+ # Check if user is authorized to write/create patients
+ # For creation, we'll check against a dummy patient object or use role-based logic
+ if current_user.role not in ["doctor", "admin"]:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Not authorized to create patients",
+ )
+
+ # If assigning to a doctor, ensure it's valid
+ if patient_data.assigned_doctor_id:
+ doctor = db.query(User).filter(User.id == patient_data.assigned_doctor_id, User.role == "doctor").first()
+ if not doctor:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Invalid doctor assignment",
+ )
+
+ db.add(db_patient)
+ db.commit()
+ db.refresh(db_patient)
+
+ # Sync OSO facts for new patient
+ try:
+ sync_patient_access(db_patient)
+ except Exception as e:
+ print(f"Warning: Failed to sync OSO facts for new patient {db_patient.id}: {e}")
+
+ return db_patient
+
+
+@router.put("/{patient_id}", response_model=PatientResponse)
+async def update_patient(
+ patient_id: int,
+ patient_update: PatientCreate,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Update patient with Oso authorization
+ """
+ oso = get_oso()
+
+ # Get the patient
+ patient = db.query(Patient).filter(Patient.id == patient_id).first()
+ if not patient:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Patient not found")
+
+ # Check if current user is authorized to write this patient
+ if not oso.authorize(current_user, "write", patient):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Not authorized to update this patient",
+ )
+
+ # Check for medical record number conflicts (if changed)
+ if patient_update.medical_record_number != patient.medical_record_number:
+ existing_patient = (
+ db.query(Patient)
+ .filter(
+ Patient.medical_record_number == patient_update.medical_record_number,
+ Patient.id != patient_id,
+ )
+ .first()
+ )
+
+ if existing_patient:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Patient with this medical record number already exists",
+ )
+
+ # Parse date_of_birth if provided
+ date_of_birth = None
+ if patient_update.date_of_birth:
+ try:
+ from datetime import datetime
+
+ date_of_birth = datetime.fromisoformat(patient_update.date_of_birth)
+ except ValueError:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Invalid date_of_birth format. Use YYYY-MM-DD",
+ )
+
+ # Track changes for OSO sync
+ old_assigned_doctor_id = patient.assigned_doctor_id
+ old_department = patient.department
+
+ # Update patient fields
+ patient.name = patient_update.name
+ patient.medical_record_number = patient_update.medical_record_number
+ patient.department = patient_update.department
+ patient.date_of_birth = date_of_birth
+ patient.assigned_doctor_id = patient_update.assigned_doctor_id
+
+ db.commit()
+ db.refresh(patient)
+
+ # Sync OSO facts if assignments changed
+ try:
+ if old_assigned_doctor_id != patient.assigned_doctor_id or old_department != patient.department:
+ # Resync all patient access facts
+ sync_patient_access(patient)
+ except Exception as e:
+ print(f"Warning: Failed to sync OSO facts for updated patient {patient.id}: {e}")
+
+ return patient
+
+
+@router.delete("/{patient_id}")
+async def delete_patient(
+ patient_id: int,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Soft delete patient (set is_active to False)
+ """
+ oso = get_oso()
+
+ # Get the patient
+ patient = db.query(Patient).filter(Patient.id == patient_id).first()
+ if not patient:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Patient not found")
+
+ # Check if current user is authorized to write this patient
+ if not oso.authorize(current_user, "write", patient):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Not authorized to delete this patient",
+ )
+
+ # Soft delete (set is_active to False)
+ patient.is_active = False
+ db.commit()
+
+ # Remove OSO facts for deactivated patient
+ try:
+ remove_patient_access(patient.id)
+ except Exception as e:
+ print(f"Warning: Failed to remove OSO facts for deactivated patient {patient.id}: {e}")
+
+ return {"message": "Patient deactivated successfully"}
+
+
+@router.get("/search/by-department/{department}", response_model=list[PatientResponse])
+async def search_patients_by_department(
+ department: str,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+ skip: int = Query(0, ge=0),
+ limit: int = Query(100, ge=1, le=1000),
+):
+ """
+ Search patients by department with authorization
+ """
+ # Use Oso Cloud to filter patients the current user can read
+ patients = (
+ db.query(Patient)
+ .options(*authorized(current_user, "read", Patient))
+ .filter(Patient.department == department, Patient.is_active)
+ .offset(skip)
+ .limit(limit)
+ .all()
+ )
+
+ return patients
diff --git a/python/rag/healthcare-support-portal/packages/rag/.env.example b/python/rag/healthcare-support-portal/packages/rag/.env.example
new file mode 100644
index 00000000..b47293d5
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/.env.example
@@ -0,0 +1,23 @@
+# RAG Service Environment Variables
+DEBUG=true
+SECRET_KEY=change-me-in-production
+DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare
+OSO_URL=http://localhost:8080
+OSO_AUTH=e_0123456789_12345_osotesttoken01xiIn
+OPENAI_API_KEY=sk-your-openai-api-key-here
+EMBEDDING_MODEL=text-embedding-3-small
+CHAT_MODEL=gpt-4o-mini
+
+# Galileo 2.0 Observability Configuration
+# Get your API key from: https://app.galileo.ai/sign-up
+GALILEO_API_KEY=your-galileo-api-key-here
+# Project name in your Galileo console
+GALILEO_PROJECT_NAME=healthcare-rag
+# Environment tag: development, staging, production
+GALILEO_ENVIRONMENT=development
+# Galileo console URL (for reference)
+GALILEO_CONSOLE_URL=https://app.galileo.ai
+
+# Logging Configuration
+LOG_LEVEL=INFO
+LOG_FORMAT=json
diff --git a/python/rag/healthcare-support-portal/packages/rag/.gitignore b/python/rag/healthcare-support-portal/packages/rag/.gitignore
new file mode 100644
index 00000000..a0516f58
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/.gitignore
@@ -0,0 +1,9 @@
+# RAG Service Specific
+.env
+*.log
+logs/
+.pytest_cache/
+__pycache__/
+*.pyc
+uploads/
+.openai_cache/
diff --git a/python/rag/healthcare-support-portal/packages/rag/RAG_GUIDE.md b/python/rag/healthcare-support-portal/packages/rag/RAG_GUIDE.md
new file mode 100644
index 00000000..fdb35069
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/RAG_GUIDE.md
@@ -0,0 +1,280 @@
+# 🧠 RAG Application:
+
+## 🎯 **What is RAG and Why It Matters**
+
+**RAG (Retrieval-Augmented Generation)** is a powerful AI technique that combines the best of both worlds:
+
+1. **🔍 Retrieval**: Find relevant information from your knowledge base
+2. **🤖 Generation**: Use AI to create intelligent, contextual responses
+
+### **Why RAG is Revolutionary**
+
+Traditional AI models have limitations:
+- ❌ **Knowledge cutoff**: They only know what they were trained on
+- ❌ **Hallucinations**: They can make up false information
+- ❌ **No source attribution**: You can't verify where information comes from
+
+RAG solves these problems by:
+- ✅ **Always up-to-date**: Uses your current documents and data
+- ✅ **Factual responses**: Grounded in real information from your knowledge base
+- ✅ **Source transparency**: Shows you exactly which documents were used
+- ✅ **Customizable**: Tailored to your specific domain and use case
+
+## 🏗️ **Your RAG System Architecture**
+
+Your RAG application follows a sophisticated, production-ready architecture:
+
+```
+┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
+│ User Query │───▶│ Vector Search │───▶│ AI Generation │
+└─────────────────┘ └─────────────────┘ └─────────────────┘
+ │ │
+ ▼ ▼
+ ┌─────────────────┐ ┌─────────────────┐
+ │ Document Store │ │ Context-Aware │
+ │ (pgvector) │ │ Response │
+ └─────────────────┘ └─────────────────┘
+```
+
+### **🔍 Step 1: Document Processing**
+```
+Upload Document → Text Extraction → Chunking → Embedding Generation → Vector Storage
+```
+
+### **🔍 Step 2: Query Processing**
+```
+User Question → Query Embedding → Similarity Search → Context Retrieval
+```
+
+### **🤖 Step 3: Response Generation**
+```
+Retrieved Context + User Role + Question → OpenAI GPT → Contextual Response
+```
+
+## 🚀 **How to Use Your RAG System**
+
+### **1. 📚 Document Management**
+
+#### **Uploading Documents**
+```bash
+# Upload a document via API
+curl -X POST "http://localhost:8003/api/v1/documents/upload" \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN" \
+ -F "file=@medical_guidelines.pdf" \
+ -F "title=Medical Guidelines 2024" \
+ -F "document_type=guidelines" \
+ -F "department=cardiology"
+```
+
+#### **Document Types Supported**
+- **PDF files**: Medical reports, guidelines, research papers
+- **Text files**: Notes, procedures, policies
+- **Word documents**: Clinical documentation
+
+#### **Automatic Processing**
+When you upload a document, the system automatically:
+1. **Extracts text** from the document
+2. **Chunks the content** into manageable pieces (1000 tokens with 200 token overlap)
+3. **Generates embeddings** using OpenAI's `text-embedding-3-small`
+4. **Stores vectors** in PostgreSQL with pgvector
+5. **Applies authorization** rules based on user roles
+
+### **2. 🔍 Searching Documents**
+
+#### **Vector Similarity Search**
+```bash
+# Search for relevant documents
+curl -X POST "http://localhost:8003/api/v1/chat/search" \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "query": "heart attack symptoms",
+ "document_types": ["guidelines", "research"],
+ "department": "cardiology",
+ "limit": 10
+ }'
+```
+
+#### **Search Features**
+- **Semantic search**: Finds documents even if they don't contain exact keywords
+- **Department filtering**: Search within specific departments
+- **Document type filtering**: Filter by guidelines, research, procedures, etc.
+- **Similarity threshold**: Configurable relevance scoring
+
+### **3. 🤖 AI-Powered Q&A**
+
+#### **Asking Questions**
+```bash
+# Ask a question and get AI-powered response
+curl -X POST "http://localhost:8003/api/v1/chat/ask" \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "message": "What are the latest treatment guidelines for diabetes?",
+ "context_patient_id": 123,
+ "context_department": "endocrinology",
+ "max_results": 5
+ }'
+```
+
+#### **Response Features**
+- **Context-aware**: Uses relevant documents to inform responses
+- **Role-based**: Tailored responses for doctors, nurses, and administrators
+- **Source attribution**: Shows which documents were used
+- **Fallback handling**: Graceful responses when no relevant context is found
+
+## 🎯 **Role-Based RAG Experience**
+
+### **👨⚕️ Doctor Experience**
+```python
+# Doctors get professional medical responses
+system_prompt = """You are an AI assistant helping a doctor in a healthcare setting.
+Provide accurate, professional medical information based on the provided context.
+Always remind users to verify information and consult current medical guidelines."""
+```
+
+**Example Doctor Query**: "What are the contraindications for prescribing metformin?"
+**RAG Response**: Uses latest medical guidelines and research papers to provide evidence-based information.
+
+### **👩⚕️ Nurse Experience**
+```python
+# Nurses get practical care information
+system_prompt = """You are an AI assistant helping a nurse in a healthcare setting.
+Provide practical, relevant information for nursing care based on the provided context.
+Focus on procedures, patient care, and safety protocols."""
+```
+
+**Example Nurse Query**: "How do I properly administer insulin to a patient?"
+**RAG Response**: Uses nursing procedures and safety guidelines for step-by-step instructions.
+
+### **👨💼 Administrator Experience**
+```python
+# Administrators get policy and procedure information
+system_prompt = """You are an AI assistant helping a healthcare administrator.
+Provide information about policies, procedures, and administrative matters based on the provided context."""
+```
+
+**Example Admin Query**: "What are the HIPAA compliance requirements for patient data?"
+**RAG Response**: Uses policy documents and compliance guidelines.
+
+## 🔐 **Security & Authorization**
+
+### **Oso Authorization System**
+Your RAG system uses Oso for fine-grained access control:
+
+```python
+# Example authorization rules
+allow(user: User, "read", document: Document) if
+ user.role == "admin" or
+ user.department == document.department or
+ document.patient_id in user.assigned_patients;
+```
+
+### **Access Control Features**
+- **Role-based access**: Different permissions for doctors, nurses, admins
+- **Department isolation**: Users only see documents from their department
+- **Patient-specific access**: Doctors only see documents for their assigned patients
+- **Sensitive document handling**: Special handling for confidential information
+
+## 📊 **Monitoring & Analytics**
+
+### **Embedding Status Tracking**
+```bash
+# Check embedding status for all documents
+curl -X GET "http://localhost:8003/api/v1/documents/embedding-statuses" \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN"
+```
+
+### **Performance Metrics**
+- **Retrieval accuracy**: How well the system finds relevant documents
+- **Response quality**: User satisfaction with AI responses
+- **Processing time**: How quickly documents are processed and queries are answered
+
+## 🛠️ **Configuration & Customization**
+
+### **Environment Variables**
+```env
+# Core RAG Configuration
+EMBEDDING_MODEL=text-embedding-3-small
+CHAT_MODEL=gpt-4o-mini
+CHUNK_SIZE=1000
+CHUNK_OVERLAP=200
+MAX_CONTEXT_LENGTH=8000
+SIMILARITY_THRESHOLD=0.7
+MAX_RESULTS=5
+```
+
+### **Customizing Chunking Strategy**
+```python
+# Adjust chunk size based on document type
+if document_type == "research_paper":
+ chunk_size = 1500 # Longer chunks for detailed content
+elif document_type == "procedure":
+ chunk_size = 500 # Shorter chunks for step-by-step procedures
+```
+
+### **Similarity Threshold Tuning**
+```python
+# Adjust based on your use case
+if user_role == "doctor":
+ similarity_threshold = 0.8 # Higher threshold for medical accuracy
+elif user_role == "nurse":
+ similarity_threshold = 0.6 # Lower threshold for broader context
+```
+
+## 🚀 **Best Practices**
+
+### **1. Document Organization**
+- **Use descriptive titles**: "Cardiology Guidelines 2024" vs "Document1"
+- **Categorize properly**: Assign correct document types and departments
+- **Keep content clean**: Remove formatting artifacts before upload
+
+### **2. Query Optimization**
+- **Be specific**: "diabetes type 2 treatment guidelines" vs "diabetes"
+- **Use context**: Specify patient or department when relevant
+- **Iterate**: Refine queries based on initial results
+
+### **3. System Maintenance**
+- **Monitor embedding status**: Ensure all documents are properly embedded
+- **Regular updates**: Keep documents current and relevant
+- **Performance monitoring**: Track query performance and user satisfaction
+
+## 🔧 **Troubleshooting**
+
+### **Common Issues**
+
+#### **No Relevant Results**
+- **Check similarity threshold**: Lower it to get more results
+- **Verify document content**: Ensure documents contain relevant information
+- **Review chunking**: Documents might be chunked inappropriately
+
+#### **Slow Response Times**
+- **Check embedding status**: Ensure all documents are embedded
+- **Optimize queries**: Use more specific search terms
+- **Monitor database performance**: Check pgvector index performance
+
+#### **Authorization Errors**
+- **Verify user permissions**: Check user role and department assignments
+- **Review Oso policies**: Ensure authorization rules are correct
+- **Check document metadata**: Verify department and patient assignments
+
+## 🎯 **Next Steps**
+
+Your RAG system is already production-ready! Here are some ways to enhance it further:
+
+1. **📈 Add Analytics**: Track usage patterns and response quality
+2. **🔄 Conversation Memory**: Remember previous interactions for better context
+3. **🎯 Personalization**: Learn user preferences and adapt responses
+4. **🚀 Performance Optimization**: Add caching and batch processing
+5. **🔍 Advanced Search**: Implement hybrid search (vector + keyword)
+
+## 📚 **Additional Resources**
+
+- **API Documentation**: http://localhost:8003/docs
+- **RAG Research**: Papers on retrieval-augmented generation
+- **OpenAI Documentation**: Embedding and chat completion APIs
+- **pgvector Documentation**: Vector similarity search in PostgreSQL
+
+---
+
+**Your RAG system is a powerful tool that combines the best of AI with your organization's knowledge. Use it wisely, and it will become an invaluable asset for your healthcare team!** 🏥✨
diff --git a/python/rag/healthcare-support-portal/packages/rag/README.md b/python/rag/healthcare-support-portal/packages/rag/README.md
new file mode 100644
index 00000000..d51f9705
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/README.md
@@ -0,0 +1,39 @@
+# RAG Service
+
+AI-powered document search and question answering service for the Healthcare Support Portal.
+
+## Features
+
+- Document embedding generation using OpenAI
+- Vector similarity search with pgvector
+- Context-aware AI responses
+- Role-based access control with Oso
+- Galileo observability integration
+
+## API Endpoints
+
+- `POST /api/v1/chat/ask` - Ask AI questions with RAG context
+- `POST /api/v1/chat/search` - Search documents by similarity
+- `POST /api/v1/documents/upload` - Upload and process documents
+- `GET /api/v1/documents/` - List authorized documents
+
+## Configuration
+
+Configure the service using environment variables in `.env`:
+
+- `OPENAI_API_KEY` - Required for embeddings and chat
+- `GALILEO_API_KEY` - Optional for observability
+- `DATABASE_URL` - PostgreSQL connection string
+- `OSO_URL` - Oso authorization server URL
+
+## Running
+
+```bash
+uv run uvicorn src.rag_service.main:app --reload --host 0.0.0.0 --port 8003
+```
+
+Or use the provided script:
+
+```bash
+./run.sh
+```
diff --git a/python/rag/healthcare-support-portal/packages/rag/pyproject.toml b/python/rag/healthcare-support-portal/packages/rag/pyproject.toml
new file mode 100644
index 00000000..15ef21f9
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/pyproject.toml
@@ -0,0 +1,34 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "rag"
+version = "0.1.0"
+description = "RAG service for Healthcare Support Portal"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+ "fastapi>=0.104.0",
+ "uvicorn[standard]>=0.24.0",
+ "pydantic>=2.0.0",
+ "pydantic-settings>=2.0.0",
+ "sqlalchemy>=2.0.0",
+ "pgvector>=0.2.0",
+ "openai>=1.0.0",
+ "tiktoken>=0.5.0",
+ "numpy>=1.24.0",
+ "python-multipart>=0.0.6",
+ "python-dotenv>=1.0.0",
+ "packaging>=21.0",
+ "common",
+ "galileo>=1.15.1",
+ "structlog>=23.0.0",
+ "rich>=13.0.0",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/rag_service"]
+
+[tool.uv.sources]
+common = { workspace = true }
diff --git a/python/rag/healthcare-support-portal/packages/rag/run.sh b/python/rag/healthcare-support-portal/packages/rag/run.sh
new file mode 100755
index 00000000..a97d7e77
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/run.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+# packages/rag/run.sh
+
+# Exit on any error
+set -e
+
+# Function to check if a command exists
+command_exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+# Load .env file if it exists
+if [ -f .env ]; then
+ echo "Loading environment variables from .env file..."
+ set -o allexport
+ source .env
+ set +o allexport
+else
+ echo "⚠️ Warning: No .env file found. Using defaults."
+fi
+
+# Set PYTHONPATH to include common package
+export PYTHONPATH="../common/src:$PYTHONPATH"
+
+# Set default environment variables if not already set
+export SECRET_KEY="${SECRET_KEY:-your-secret-key-here}"
+export DATABASE_URL="${DATABASE_URL:-postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare}"
+export DEBUG="${DEBUG:-true}"
+export HOST="${HOST:-0.0.0.0}"
+export PORT="${PORT:-8003}"
+
+# OpenAI Configuration
+export OPENAI_API_KEY="${OPENAI_API_KEY:-}"
+export EMBEDDING_MODEL="${EMBEDDING_MODEL:-text-embedding-3-small}"
+export CHAT_MODEL="${CHAT_MODEL:-gpt-4o-mini}"
+
+# RAG Configuration
+export CHUNK_SIZE="${CHUNK_SIZE:-1000}"
+export CHUNK_OVERLAP="${CHUNK_OVERLAP:-200}"
+export MAX_CONTEXT_LENGTH="${MAX_CONTEXT_LENGTH:-8000}"
+export SIMILARITY_THRESHOLD="${SIMILARITY_THRESHOLD:-0.7}"
+export MAX_RESULTS="${MAX_RESULTS:-5}"
+
+echo "🤖 Starting RAG Service on port $PORT..."
+echo "📊 Debug mode: $DEBUG"
+echo "🔑 Using SECRET_KEY: ${SECRET_KEY:0:10}..."
+echo "🗄️ Database: ${DATABASE_URL%%@*}@***"
+echo "🧠 OpenAI API Key: ${OPENAI_API_KEY:0:10}${OPENAI_API_KEY:10:1}..."
+echo "📝 Embedding Model: $EMBEDDING_MODEL"
+echo "💬 Chat Model: $CHAT_MODEL"
+echo "🔍 Similarity Threshold: $SIMILARITY_THRESHOLD"
+
+# Check if OpenAI API key is set
+if [ -z "$OPENAI_API_KEY" ]; then
+ echo "⚠️ WARNING: OPENAI_API_KEY is not set. RAG functionality will not work."
+ echo " Please set your OpenAI API key in the .env file or environment variables."
+fi
+
+# Check for uv command
+UV_CMD="uv"
+if [ -f "/opt/homebrew/bin/uv" ]; then
+ UV_CMD="/opt/homebrew/bin/uv"
+fi
+
+if ! command_exists "$UV_CMD"; then
+ echo "❌ uv package manager not found. Please install uv and try again."
+ echo " Installation: curl -LsSf https://astral.sh/uv/install.sh | sh"
+ exit 1
+fi
+
+# Check if port is available
+if command_exists lsof; then
+ if lsof -i :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
+ echo "❌ Port $PORT is already in use. Please stop the service using this port."
+ exit 1
+ fi
+fi
+
+# Check essential environment variables
+if [ "$SECRET_KEY" = "your-secret-key-here" ] || [ "$SECRET_KEY" = "change-me-in-production" ]; then
+ echo "⚠️ WARNING: Using default SECRET_KEY. This is insecure for production!"
+fi
+
+# Run the RAG service
+echo "🚀 Starting uvicorn server..."
+if ! $UV_CMD run uvicorn src.rag_service.main:app --reload --host $HOST --port $PORT; then
+ echo "❌ Failed to start RAG Service"
+ exit 1
+fi
diff --git a/python/rag/healthcare-support-portal/packages/rag/src/rag_service/__init__.py b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/__init__.py
new file mode 100644
index 00000000..38058a03
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/__init__.py
@@ -0,0 +1,6 @@
+"""
+RAG Service for Healthcare Support Portal
+Handles document management, embeddings, and AI-powered responses
+"""
+
+__version__ = "0.1.0"
diff --git a/python/rag/healthcare-support-portal/packages/rag/src/rag_service/config.py b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/config.py
new file mode 100644
index 00000000..eebb9321
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/config.py
@@ -0,0 +1,57 @@
+import os
+
+from dotenv import load_dotenv
+from pydantic_settings import BaseSettings
+
+load_dotenv()
+
+
+class Settings(BaseSettings):
+ app_name: str = "Healthcare Support Portal - RAG Service"
+ debug: bool = os.getenv("DEBUG", "False").lower() == "true"
+ host: str = "0.0.0.0"
+ port: int = 8003
+
+ # Database
+ database_url: str = os.getenv(
+ "DATABASE_URL",
+ "postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare",
+ )
+
+ # JWT
+ secret_key: str = os.getenv("SECRET_KEY", "your-secret-key-here")
+
+ # OpenAI
+ openai_api_key: str = os.getenv("OPENAI_API_KEY", "")
+ embedding_model: str = "text-embedding-3-small"
+ chat_model: str = "gpt-4o-mini"
+
+ # RAG Configuration
+ chunk_size: int = 1000
+ chunk_overlap: int = 200
+ max_context_length: int = 8000
+ similarity_threshold: float = 0.3
+ max_results: int = 5
+
+ # Oso Configuration
+ oso_url: str = os.getenv("OSO_URL", "http://localhost:8080")
+ oso_auth: str = os.getenv("OSO_AUTH", "e_0123456789_12345_osotesttoken01xiIn")
+
+ # Galileo 2.0 Observability Configuration
+ galileo_enabled: bool = os.getenv("GALILEO_ENABLED", "true").lower() == "true"
+ galileo_api_key: str = os.getenv("GALILEO_API_KEY", "")
+ galileo_project_name: str = os.getenv("GALILEO_PROJECT_NAME", "healthcare-rag")
+ galileo_environment: str = os.getenv("GALILEO_ENVIRONMENT", "development")
+
+ # Logging Configuration
+ log_level: str = os.getenv("LOG_LEVEL", "INFO")
+ log_format: str = os.getenv("LOG_FORMAT", "json") # json or console
+
+ class Config:
+ env_file = ".env"
+ # Explicitly ignore extra fields to prevent OpenTelemetry/Prometheus
+ # env vars from being loaded
+ extra = "ignore"
+
+
+settings = Settings()
diff --git a/python/rag/healthcare-support-portal/packages/rag/src/rag_service/main.py b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/main.py
new file mode 100644
index 00000000..25427fbb
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/main.py
@@ -0,0 +1,104 @@
+import sys
+from pathlib import Path
+
+# Add the common package to Python path
+common_path = Path(__file__).parent.parent.parent.parent / "common" / "src"
+sys.path.insert(0, str(common_path))
+
+import sqlalchemy_oso_cloud
+from common.migration_check import require_migrations_current
+from common.models import Base
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from galileo.openai import openai
+
+from .config import settings
+from .observability import initialize_observability, ObservabilityMiddleware, logger
+from .routers import chat, documents
+
+# Set OpenAI API key
+openai.api_key = settings.openai_api_key
+
+# Initialize SQLAlchemy Oso Cloud with registry and server settings
+# Add error handling for development environments where OSO might not be available
+try:
+ sqlalchemy_oso_cloud.init(Base.registry, url=settings.oso_url, api_key=settings.oso_auth)
+ print(f"✅ OSO Cloud initialized: {settings.oso_url}")
+except Exception as e:
+ print(f"⚠️ OSO Cloud initialization failed: {e}")
+ print("🔧 Authorization will be disabled - use for development only!")
+ # You may want to set a flag here to disable authorization checks
+
+# Create FastAPI app
+app = FastAPI(
+ title=settings.app_name,
+ description="RAG service with vector search and AI-powered responses",
+ version="0.1.0",
+ debug=settings.debug,
+)
+
+# Add observability middleware
+app.add_middleware(ObservabilityMiddleware)
+
+# Add CORS middleware
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # Configure appropriately for production
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Include routers
+app.include_router(documents.router, prefix="/api/v1/documents", tags=["Documents"])
+app.include_router(chat.router, prefix="/api/v1/chat", tags=["Chat"])
+
+
+@app.on_event("startup")
+async def startup_event():
+ """Verify migrations and start service"""
+ try:
+ # Initialize observability components
+ initialize_observability()
+
+ # Verify database migrations are current
+ require_migrations_current()
+
+ logger.info(f"🚀 {settings.app_name} started successfully", port=settings.port, galileo_enabled=settings.galileo_enabled)
+
+ except Exception as e:
+ logger.error(f"Failed to start {settings.app_name}", error=str(e))
+ raise
+
+
+@app.get("/")
+async def root():
+ return {
+ "service": "rag_service",
+ "status": "healthy",
+ "version": "0.1.0",
+ "observability": {"galileo_enabled": settings.galileo_enabled},
+ }
+
+
+@app.get("/health")
+async def health_check():
+ return {"status": "healthy"}
+
+
+@app.get("/observability")
+async def observability_status():
+ """Get observability configuration status"""
+ return {
+ "galileo": {
+ "enabled": settings.galileo_enabled,
+ "project": settings.galileo_project_name,
+ "environment": settings.galileo_environment,
+ },
+ "logging": {"level": settings.log_level, "format": settings.log_format},
+ }
+
+
+# Make Oso Cloud instance and settings available to routes
+app.state.oso_sqlalchemy = sqlalchemy_oso_cloud
+app.state.settings = settings
diff --git a/python/rag/healthcare-support-portal/packages/rag/src/rag_service/observability.py b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/observability.py
new file mode 100644
index 00000000..7ffaa39b
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/observability.py
@@ -0,0 +1,553 @@
+"""
+Observability module for RAG service using Galileo 2.0 and structured logging.
+"""
+
+import time
+import uuid
+from contextlib import asynccontextmanager
+from typing import Any, Dict, Optional
+
+import structlog
+from fastapi import Request
+
+# Import Galileo modules correctly
+from galileo import GalileoLogger, galileo_context
+import os
+
+from .config import settings
+
+# Initialize structured logging
+structlog.configure(
+ processors=[
+ structlog.stdlib.filter_by_level,
+ structlog.stdlib.add_logger_name,
+ structlog.stdlib.add_log_level,
+ structlog.stdlib.PositionalArgumentsFormatter(),
+ structlog.processors.TimeStamper(fmt="iso"),
+ structlog.processors.StackInfoRenderer(),
+ structlog.processors.format_exc_info,
+ structlog.processors.UnicodeDecoder(),
+ structlog.processors.JSONRenderer() if settings.log_format == "json" else structlog.dev.ConsoleRenderer(),
+ ],
+ context_class=dict,
+ logger_factory=structlog.stdlib.LoggerFactory(),
+ wrapper_class=structlog.stdlib.BoundLogger,
+ cache_logger_on_first_use=True,
+)
+
+logger = structlog.get_logger(__name__)
+
+# Galileo logging enabled flag
+galileo_enabled: bool = False
+galileo_logger: GalileoLogger = None
+
+
+def initialize_observability():
+ """Initialize all observability components."""
+ global galileo_enabled, galileo_logger
+
+ logger.info("Initializing observability components")
+
+ # Read Galileo configuration from settings
+ galileo_api_key = settings.galileo_api_key
+ galileo_project_name = settings.galileo_project_name
+
+ # Initialize Galileo logging if enabled and API key is provided
+ if settings.galileo_enabled and galileo_api_key and galileo_api_key.strip():
+ try:
+ # Set Galileo environment variables for the modern API
+ os.environ["GALILEO_API_KEY"] = galileo_api_key
+ os.environ["GALILEO_PROJECT_NAME"] = galileo_project_name
+ os.environ["GALILEO_ENVIRONMENT"] = settings.galileo_environment
+
+ # Initialize GalileoLogger
+ galileo_logger = GalileoLogger(project=galileo_project_name, log_stream="rag-service")
+
+ galileo_enabled = True
+ logger.info(
+ "Galileo logging initialized successfully",
+ project=galileo_project_name,
+ environment=settings.galileo_environment,
+ )
+ except Exception as e:
+ logger.error("Failed to initialize Galileo logging", error=str(e))
+ galileo_enabled = False
+ galileo_logger = None
+ else:
+ logger.info("Galileo observability disabled or API key not configured")
+ galileo_enabled = False
+ galileo_logger = None
+
+
+@asynccontextmanager
+async def rag_query_context(query_type: str, user_role: str, department: str = None, query_id: str = None):
+ """Context manager for RAG query observability."""
+ if query_id is None:
+ query_id = str(uuid.uuid4())
+
+ start_time = time.time()
+ trace = None
+
+ try:
+ # Log query start
+ logger.info("RAG query started", query_type=query_type, user_role=user_role, department=department, query_id=query_id)
+
+ # Start Galileo trace if enabled
+ if galileo_enabled and galileo_logger:
+ try:
+ # Start a session if not already started
+ session_id = galileo_logger.start_session(name=f"RAG-{query_type}", external_id=query_id)
+
+ # Start a trace
+ trace = galileo_logger.start_trace(
+ input=f"RAG {query_type} query",
+ name=f"RAG Query - {query_type}",
+ tags=[query_type, user_role, department] if department else [query_type, user_role],
+ metadata={
+ "query_type": query_type,
+ "user_role": user_role,
+ "department": department,
+ "query_id": query_id,
+ },
+ )
+ except Exception as e:
+ logger.warning("Failed to start Galileo trace", error=str(e))
+
+ yield query_id
+
+ except Exception as e:
+ # Log error
+ logger.error(
+ "RAG query failed",
+ query_type=query_type,
+ user_role=user_role,
+ department=department,
+ query_id=query_id,
+ error=str(e),
+ )
+
+ # Add error information to Galileo trace if enabled
+ if galileo_enabled and galileo_logger and trace:
+ try:
+ # Add an agent span to capture the error
+ galileo_logger.add_agent_span(
+ input=f"RAG {query_type} query",
+ output=f"Error: {str(e)}",
+ name=f"RAG Error - {query_type}",
+ metadata={"error_type": type(e).__name__, "error": str(e)},
+ tags=["error", query_type],
+ )
+ except Exception as galileo_error:
+ logger.warning("Failed to log error to Galileo", error=str(galileo_error))
+
+ raise
+
+ finally:
+ # Calculate duration
+ duration = time.time() - start_time
+ duration_ns = int(duration * 1_000_000_000)
+
+ # Log query completion
+ logger.info(
+ "RAG query completed",
+ query_type=query_type,
+ user_role=user_role,
+ department=department,
+ query_id=query_id,
+ duration=duration,
+ )
+
+ # Conclude Galileo trace if enabled
+ if galileo_enabled and galileo_logger and trace:
+ try:
+ galileo_logger.conclude(output="RAG query completed", duration_ns=duration_ns)
+ galileo_logger.flush()
+ except Exception as galileo_error:
+ logger.warning("Failed to conclude Galileo trace", error=str(galileo_error))
+
+
+@asynccontextmanager
+async def embedding_generation_context(model: str, chunk_count: int, operation_id: str = None):
+ """Context manager for embedding generation observability."""
+ if operation_id is None:
+ operation_id = str(uuid.uuid4())
+
+ start_time = time.time()
+
+ try:
+ logger.info("Embedding generation started", model=model, chunk_count=chunk_count, operation_id=operation_id)
+
+ # Log to Galileo
+ log_galileo_event(
+ event_type="embedding_generation_started",
+ event_data={
+ "model": model,
+ "chunk_count": chunk_count,
+ "operation_id": operation_id,
+ "timestamp": start_time,
+ },
+ )
+
+ yield operation_id
+
+ except Exception as e:
+ logger.error("Embedding generation failed", model=model, chunk_count=chunk_count, operation_id=operation_id, error=str(e))
+
+ # Log error to Galileo
+ log_galileo_event(
+ event_type="embedding_generation_failed",
+ event_data={
+ "model": model,
+ "chunk_count": chunk_count,
+ "operation_id": operation_id,
+ "error": str(e),
+ "error_type": type(e).__name__,
+ "timestamp": time.time(),
+ },
+ )
+
+ raise
+
+ finally:
+ duration = time.time() - start_time
+
+ logger.info(
+ "Embedding generation completed",
+ model=model,
+ chunk_count=chunk_count,
+ operation_id=operation_id,
+ duration=duration,
+ )
+
+ # Log completion to Galileo
+ log_galileo_event(
+ event_type="embedding_generation_completed",
+ event_data={
+ "model": model,
+ "chunk_count": chunk_count,
+ "operation_id": operation_id,
+ "duration": duration,
+ "timestamp": time.time(),
+ },
+ )
+
+
+@asynccontextmanager
+async def vector_search_context(result_count: int, similarity_threshold: float, search_id: str = None):
+ """Context manager for vector search observability."""
+ if search_id is None:
+ search_id = str(uuid.uuid4())
+
+ start_time = time.time()
+
+ try:
+ logger.info(
+ "Vector search started",
+ result_count=result_count,
+ similarity_threshold=similarity_threshold,
+ search_id=search_id,
+ )
+
+ # Log to Galileo
+ log_galileo_event(
+ event_type="vector_search_started",
+ event_data={
+ "result_count": result_count,
+ "similarity_threshold": similarity_threshold,
+ "search_id": search_id,
+ "timestamp": start_time,
+ },
+ )
+
+ yield search_id
+
+ except Exception as e:
+ logger.error(
+ "Vector search failed",
+ result_count=result_count,
+ similarity_threshold=similarity_threshold,
+ search_id=search_id,
+ error=str(e),
+ )
+
+ # Log error to Galileo
+ log_galileo_event(
+ event_type="vector_search_failed",
+ event_data={
+ "result_count": result_count,
+ "similarity_threshold": similarity_threshold,
+ "search_id": search_id,
+ "error": str(e),
+ "error_type": type(e).__name__,
+ "timestamp": time.time(),
+ },
+ )
+
+ raise
+
+ finally:
+ duration = time.time() - start_time
+
+ logger.info(
+ "Vector search completed",
+ result_count=result_count,
+ similarity_threshold=similarity_threshold,
+ search_id=search_id,
+ duration=duration,
+ )
+
+ # Log completion to Galileo
+ log_galileo_event(
+ event_type="vector_search_completed",
+ event_data={
+ "result_count": result_count,
+ "similarity_threshold": similarity_threshold,
+ "search_id": search_id,
+ "duration": duration,
+ "timestamp": time.time(),
+ },
+ )
+
+
+@asynccontextmanager
+async def ai_response_context(model: str, token_count: int, response_id: str = None):
+ """Context manager for AI response generation observability."""
+ if response_id is None:
+ response_id = str(uuid.uuid4())
+
+ start_time = time.time()
+
+ try:
+ logger.info("AI response generation started", model=model, token_count=token_count, response_id=response_id)
+
+ # Log to Galileo
+ log_galileo_event(
+ event_type="ai_response_generation_started",
+ event_data={
+ "model": model,
+ "token_count": token_count,
+ "response_id": response_id,
+ "timestamp": start_time,
+ },
+ )
+
+ yield response_id
+
+ except Exception as e:
+ logger.error("AI response generation failed", model=model, token_count=token_count, response_id=response_id, error=str(e))
+
+ # Log error to Galileo
+ log_galileo_event(
+ event_type="ai_response_generation_failed",
+ event_data={
+ "model": model,
+ "token_count": token_count,
+ "response_id": response_id,
+ "error": str(e),
+ "error_type": type(e).__name__,
+ "timestamp": time.time(),
+ },
+ )
+
+ raise
+
+ finally:
+ duration = time.time() - start_time
+
+ logger.info(
+ "AI response generation completed",
+ model=model,
+ token_count=token_count,
+ response_id=response_id,
+ duration=duration,
+ )
+
+ # Log completion to Galileo
+ log_galileo_event(
+ event_type="ai_response_generation_completed",
+ event_data={
+ "model": model,
+ "token_count": token_count,
+ "response_id": response_id,
+ "duration": duration,
+ "timestamp": time.time(),
+ },
+ )
+
+
+def log_llm_call(
+ input_text: str,
+ output_text: str,
+ model: str,
+ num_input_tokens: int = None,
+ num_output_tokens: int = None,
+ total_tokens: int = None,
+ duration_ns: int = None,
+ temperature: float = None,
+):
+ """Log an LLM call to Galileo."""
+ if galileo_enabled and galileo_logger:
+ try:
+ galileo_logger.add_llm_span(
+ input=input_text,
+ output=output_text,
+ model=model,
+ name=f"LLM Call - {model}",
+ num_input_tokens=num_input_tokens,
+ num_output_tokens=num_output_tokens,
+ total_tokens=total_tokens,
+ duration_ns=duration_ns,
+ temperature=temperature,
+ tags=["llm", str(model)],
+ metadata={
+ "model": str(model),
+ "temperature": str(temperature) if temperature else "0.7",
+ "input_tokens": str(num_input_tokens) if num_input_tokens else "unknown",
+ "output_tokens": str(num_output_tokens) if num_output_tokens else "unknown",
+ "total_tokens": str(total_tokens) if total_tokens else "unknown",
+ },
+ )
+ logger.debug("LLM call logged to Galileo", model=model)
+ except Exception as e:
+ logger.warning("Failed to log LLM call to Galileo", error=str(e))
+
+
+def log_retriever_call(query: str, documents: list, duration_ns: int = None):
+ """Log a retriever call to Galileo."""
+ if galileo_enabled and galileo_logger:
+ try:
+ galileo_logger.add_retriever_span(
+ input=query,
+ output=documents,
+ name="Document Retrieval",
+ duration_ns=duration_ns,
+ tags=["retriever", "rag"],
+ metadata={
+ "query": str(query),
+ "document_count": str(len(documents)) if documents else "0",
+ "duration_ms": str(duration_ns / 1_000_000) if duration_ns else "unknown",
+ },
+ )
+ logger.debug("Retriever call logged to Galileo", doc_count=len(documents) if documents else 0)
+ except Exception as e:
+ logger.warning("Failed to log retriever call to Galileo", error=str(e))
+
+
+def log_document_upload(document_type: str, department: str, file_size: int, document_id: int):
+ """Log document upload metrics."""
+ logger.info(
+ "Document uploaded",
+ document_type=document_type,
+ department=department,
+ file_size=file_size,
+ document_id=document_id,
+ )
+
+ # Log to Galileo
+ log_galileo_event(
+ event_type="document_uploaded",
+ event_data={
+ "document_type": document_type,
+ "department": department,
+ "file_size": file_size,
+ "document_id": document_id,
+ "timestamp": time.time(),
+ },
+ )
+
+
+def log_embeddings_stored(document_id: int, chunk_count: int):
+ """Log embeddings storage metrics."""
+ logger.info("Embeddings stored", document_id=document_id, chunk_count=chunk_count)
+
+ # Log to Galileo
+ log_galileo_event(
+ event_type="embeddings_stored",
+ event_data={"document_id": document_id, "chunk_count": chunk_count, "timestamp": time.time()},
+ )
+
+
+def log_galileo_event(event_type: str, event_data: Dict[str, Any], user_id: str = None, session_id: str = None):
+ """Log event to Galileo using the GalileoLogger."""
+ if galileo_enabled and galileo_logger:
+ try:
+ # Create a workflow span for general events
+ message = event_data.get("query", event_data.get("message", f"{event_type} event"))
+
+ # Add a workflow span to capture the event
+ # Convert all metadata values to strings for Galileo validation
+ metadata = {
+ "event_type": str(event_type),
+ "user_id": str(user_id) if user_id else "unknown",
+ "session_id": str(session_id or str(uuid.uuid4())),
+ "timestamp": str(time.time()),
+ }
+
+ # Convert event_data values to strings
+ for key, value in event_data.items():
+ metadata[key] = str(value) if value is not None else "null"
+
+ galileo_logger.add_workflow_span(
+ input=message,
+ output=f"Event: {event_type}",
+ name=event_type,
+ metadata=metadata,
+ tags=[event_type, "event"],
+ )
+
+ logger.debug("Event logged to Galileo", event_type=event_type, user_id=user_id, session_id=session_id)
+ except Exception as e:
+ logger.error("Failed to log event to Galileo", event_type=event_type, error=str(e))
+
+
+class ObservabilityMiddleware:
+ """FastAPI middleware for observability."""
+
+ def __init__(self, app):
+ self.app = app
+
+ async def __call__(self, scope, receive, send):
+ if scope["type"] == "http":
+ request = Request(scope, receive)
+
+ start_time = time.time()
+
+ try:
+ await self.app(scope, receive, send)
+ except Exception as e:
+ # Log error to Galileo
+ log_galileo_event(
+ event_type="http_request_failed",
+ event_data={
+ "method": request.method,
+ "path": request.url.path,
+ "error": str(e),
+ "error_type": type(e).__name__,
+ "timestamp": time.time(),
+ },
+ )
+ raise
+ finally:
+ duration = time.time() - start_time
+
+ logger.info(
+ "HTTP request completed",
+ method=request.method,
+ path=request.url.path,
+ duration=duration,
+ status_code=getattr(scope, "status_code", 500),
+ )
+
+ # Log to Galileo
+ log_galileo_event(
+ event_type="http_request_completed",
+ event_data={
+ "method": request.method,
+ "path": request.url.path,
+ "duration": duration,
+ "status_code": getattr(scope, "status_code", 500),
+ "timestamp": time.time(),
+ },
+ )
+ else:
+ await self.app(scope, receive, send)
diff --git a/python/rag/healthcare-support-portal/packages/rag/src/rag_service/routers/__init__.py b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/routers/__init__.py
new file mode 100644
index 00000000..5bb3dc22
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/routers/__init__.py
@@ -0,0 +1 @@
+# Router package initialization
diff --git a/python/rag/healthcare-support-portal/packages/rag/src/rag_service/routers/chat.py b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/routers/chat.py
new file mode 100644
index 00000000..0b34ce77
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/routers/chat.py
@@ -0,0 +1,425 @@
+import sys
+from pathlib import Path
+
+# Add the common package to Python path
+common_path = Path(__file__).parent.parent.parent.parent.parent / "common" / "src"
+sys.path.insert(0, str(common_path))
+
+
+from galileo.openai import openai
+from common.auth import get_current_user
+from common.db import get_db
+from common.models import Document, User
+from fastapi import APIRouter, Depends, HTTPException, Request, status
+from pydantic import BaseModel
+from sqlalchemy.orm import Session
+from sqlalchemy_oso_cloud import authorized
+
+from ..utils.embeddings import combine_chunks_for_context, similarity_search
+from ..utils.text_processing import calculate_token_count
+from ..observability import rag_query_context, vector_search_context, ai_response_context, log_galileo_event, logger
+
+router = APIRouter()
+
+
+# Request/Response Models
+class ChatRequest(BaseModel):
+ message: str
+ context_patient_id: int | None = None
+ context_department: str | None = None
+ max_results: int | None = 5
+
+
+class ChatResponse(BaseModel):
+ response: str
+ sources: list[dict]
+ token_count: int
+ context_used: bool
+
+
+class SearchRequest(BaseModel):
+ query: str
+ document_types: list[str] | None = None
+ department: str | None = None
+ limit: int | None = 10
+
+
+class SearchResponse(BaseModel):
+ results: list[dict]
+ total_results: int
+
+
+@router.post("/search", response_model=SearchResponse)
+async def search_documents(
+ search_request: SearchRequest,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Search documents using vector similarity
+ """
+ settings = request.app.state.settings
+
+ async with rag_query_context(query_type="search", user_role=current_user.role, department=search_request.department) as query_id:
+
+ # Get authorized documents query with OSO fallback
+ try:
+ authorized_query = db.query(Document).options(authorized(current_user, "read", Document))
+ except Exception as oso_error:
+ logger.warning(
+ "OSO authorization failed in search, falling back to basic query",
+ query_id=query_id,
+ error=str(oso_error),
+ user_role=current_user.role,
+ )
+ # Fallback to basic role-based filtering
+ authorized_query = db.query(Document)
+ if current_user.role != "admin":
+ authorized_query = authorized_query.filter(Document.department == current_user.department)
+
+ # Apply filters
+ if search_request.document_types:
+ authorized_query = authorized_query.filter(Document.document_type.in_(search_request.document_types))
+
+ if search_request.department:
+ authorized_query = authorized_query.filter(Document.department == search_request.department)
+
+ # Get authorized document IDs
+ authorized_docs = authorized_query.all()
+ authorized_doc_ids = [doc.id for doc in authorized_docs]
+
+ if not authorized_doc_ids:
+ logger.info(
+ "No authorized documents found for search",
+ query_id=query_id,
+ user_role=current_user.role,
+ department=search_request.department,
+ )
+ return SearchResponse(results=[], total_results=0)
+
+ # Perform similarity search with observability
+ async with vector_search_context(
+ result_count=search_request.limit or settings.max_results,
+ similarity_threshold=settings.similarity_threshold,
+ ) as search_id:
+
+ results = await similarity_search(
+ query_text=search_request.query,
+ db=db,
+ limit=search_request.limit or settings.max_results,
+ similarity_threshold=settings.similarity_threshold,
+ document_ids=authorized_doc_ids,
+ )
+
+ # Log to Galileo
+ log_galileo_event(
+ event_type="document_search",
+ event_data={
+ "query": search_request.query,
+ "document_types": search_request.document_types,
+ "department": search_request.department,
+ "results_count": len(results),
+ "authorized_docs_count": len(authorized_doc_ids),
+ "search_id": search_id,
+ },
+ user_id=str(current_user.id),
+ session_id=query_id,
+ )
+
+ logger.info(
+ "Document search completed",
+ query_id=query_id,
+ search_id=search_id,
+ results_count=len(results),
+ user_role=current_user.role,
+ )
+
+ return SearchResponse(results=results, total_results=len(results))
+
+
+@router.post("/ask", response_model=ChatResponse)
+async def ask_question(
+ chat_request: ChatRequest,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Ask a question and get an AI-powered response with RAG
+ """
+ settings = request.app.state.settings
+
+ async with rag_query_context(query_type="ask", user_role=current_user.role, department=chat_request.context_department) as query_id:
+
+ # Get authorized documents for context with OSO fallback
+ try:
+ authorized_query = db.query(Document).options(authorized(current_user, "read", Document))
+ except Exception as oso_error:
+ logger.warning(
+ "OSO authorization failed in ask question, falling back to basic query",
+ query_id=query_id,
+ error=str(oso_error),
+ user_role=current_user.role,
+ )
+ # Fallback to basic role-based filtering
+ authorized_query = db.query(Document)
+ if current_user.role != "admin":
+ authorized_query = authorized_query.filter(Document.department == current_user.department)
+
+ # Apply context filters if provided
+ if chat_request.context_patient_id:
+ authorized_query = authorized_query.filter(Document.patient_id == chat_request.context_patient_id)
+
+ if chat_request.context_department:
+ authorized_query = authorized_query.filter(Document.department == chat_request.context_department)
+
+ # Get authorized document IDs
+ authorized_docs = authorized_query.all()
+ authorized_doc_ids = [doc.id for doc in authorized_docs]
+
+ sources = []
+ context_used = False
+
+ if authorized_doc_ids:
+ # Perform similarity search with observability
+ async with vector_search_context(
+ result_count=chat_request.max_results or settings.max_results,
+ similarity_threshold=settings.similarity_threshold,
+ ) as search_id:
+
+ search_results = await similarity_search(
+ query_text=chat_request.message,
+ db=db,
+ limit=chat_request.max_results or settings.max_results,
+ similarity_threshold=settings.similarity_threshold,
+ document_ids=authorized_doc_ids,
+ )
+
+ sources = search_results
+ context_used = len(search_results) > 0
+
+ logger.info(
+ "Vector search completed for AI question",
+ query_id=query_id,
+ search_id=search_id,
+ sources_count=len(sources),
+ context_used=context_used,
+ )
+
+ # Generate AI response with observability
+ try:
+ token_count = calculate_token_count(chat_request.message)
+
+ async with ai_response_context(model=settings.chat_model, token_count=token_count) as response_id:
+
+ ai_response = await generate_ai_response(
+ question=chat_request.message,
+ context_results=sources,
+ user_role=current_user.role,
+ settings=settings,
+ )
+
+ final_token_count = calculate_token_count(ai_response)
+
+ # Log to Galileo
+ log_galileo_event(
+ event_type="ai_question_answered",
+ event_data={
+ "question": chat_request.message,
+ "context_used": context_used,
+ "sources_count": len(sources),
+ "response_length": len(ai_response),
+ "input_tokens": token_count,
+ "output_tokens": final_token_count,
+ "model": settings.chat_model,
+ "response_id": response_id,
+ },
+ user_id=str(current_user.id),
+ session_id=query_id,
+ )
+
+ logger.info(
+ "AI question answered successfully",
+ query_id=query_id,
+ response_id=response_id,
+ context_used=context_used,
+ sources_count=len(sources),
+ user_role=current_user.role,
+ )
+
+ return ChatResponse(
+ response=ai_response,
+ sources=sources,
+ token_count=final_token_count,
+ context_used=context_used,
+ )
+
+ except Exception as e:
+ logger.error("Failed to generate AI response", query_id=query_id, error=str(e), user_role=current_user.role)
+
+ # Log error to Galileo
+ log_galileo_event(
+ event_type="ai_response_error",
+ event_data={"question": chat_request.message, "error": str(e), "error_type": type(e).__name__},
+ user_id=str(current_user.id),
+ session_id=query_id,
+ )
+
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error generating response: {str(e)}",
+ )
+
+
+async def generate_ai_response(question: str, context_results: list[dict], user_role: str, settings) -> str:
+ """
+ Generate AI response using OpenAI with RAG context
+ """
+ # Prepare context from search results
+ context = ""
+ if context_results:
+ context = combine_chunks_for_context(context_results, max_tokens=settings.max_context_length)
+
+ # Create system prompt based on user role
+ system_prompts = {
+ "doctor": """You are an AI assistant helping a doctor in a healthcare setting.
+ Provide accurate, professional medical information based on the provided context.
+ Always remind users to verify information and consult current medical guidelines.""",
+ "nurse": """You are an AI assistant helping a nurse in a healthcare setting.
+ Provide practical, relevant information for nursing care based on the provided context.
+ Focus on procedures, patient care, and safety protocols.""",
+ "admin": """You are an AI assistant helping a healthcare administrator.
+ Provide information about policies, procedures, and administrative matters based on the provided context.""",
+ }
+
+ system_prompt = system_prompts.get(user_role, system_prompts["admin"])
+
+ # Prepare messages
+ messages = [
+ {"role": "system", "content": system_prompt},
+ ]
+
+ if context:
+ messages.append(
+ {
+ "role": "system",
+ "content": f"Use the following context to answer the user's question:\n\n{context}",
+ }
+ )
+
+ messages.append({"role": "user", "content": question})
+
+ # Generate response using OpenAI (Galileo instrumented)
+ import time
+
+ start_time = time.time()
+
+ try:
+ client = openai.OpenAI(api_key=settings.openai_api_key)
+ response = client.chat.completions.create(
+ model=settings.chat_model,
+ messages=messages,
+ # max_tokens=1000,
+ temperature=0.7,
+ )
+
+ ai_response = response.choices[0].message.content
+
+ # Calculate duration and tokens
+ duration_ns = int((time.time() - start_time) * 1_000_000_000)
+ input_tokens = response.usage.prompt_tokens if hasattr(response, "usage") else None
+ output_tokens = response.usage.completion_tokens if hasattr(response, "usage") else None
+ total_tokens = response.usage.total_tokens if hasattr(response, "usage") else None
+
+ # Log LLM call to Galileo
+ try:
+ from ..observability import log_llm_call
+
+ # Create full input context for logging
+ full_input = "\n\n".join([msg["content"] for msg in messages])
+
+ log_llm_call(
+ input_text=full_input,
+ output_text=ai_response,
+ model=settings.chat_model,
+ num_input_tokens=input_tokens,
+ num_output_tokens=output_tokens,
+ total_tokens=total_tokens,
+ duration_ns=duration_ns,
+ temperature=0.7,
+ )
+ except Exception as log_error:
+ print(f"Warning: Failed to log LLM call to Galileo: {log_error}")
+
+ # Add disclaimer if no context was used
+ if not context:
+ ai_response += (
+ "\n\n*Note: This response was generated without specific document context. Please verify information with current medical guidelines.*"
+ )
+
+ return ai_response
+
+ except Exception as e:
+ error_msg = f"I apologize, but I'm unable to generate a response at this time. Error: {str(e)}"
+
+ # Log error to Galileo
+ try:
+ from ..observability import log_llm_call
+
+ duration_ns = int((time.time() - start_time) * 1_000_000_000)
+ full_input = "\n\n".join([msg["content"] for msg in messages])
+
+ log_llm_call(
+ input_text=full_input,
+ output_text=error_msg,
+ model=settings.chat_model,
+ duration_ns=duration_ns,
+ temperature=0.7,
+ )
+ except Exception as log_error:
+ print(f"Warning: Failed to log LLM error to Galileo: {log_error}")
+
+ return error_msg
+
+
+@router.get("/conversation-history")
+async def get_conversation_history(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
+ """
+ Get conversation history for the current user
+ Note: In a full implementation, you'd store conversation history in the database
+ """
+ # Placeholder - would implement conversation storage
+ return {
+ "message": "Conversation history feature not yet implemented",
+ "user_id": current_user.id,
+ }
+
+
+@router.post("/feedback")
+async def submit_feedback(
+ response_id: str,
+ rating: int,
+ feedback: str | None = None,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Submit feedback on AI responses
+ Note: In a full implementation, you'd store feedback in the database
+ """
+ # Log feedback to Galileo
+ log_galileo_event(
+ event_type="ai_response_feedback",
+ event_data={"response_id": response_id, "rating": rating, "feedback": feedback},
+ user_id=str(current_user.id),
+ )
+
+ logger.info("AI response feedback submitted", response_id=response_id, rating=rating, user_role=current_user.role)
+
+ # Placeholder - would implement feedback storage
+ return {
+ "message": "Thank you for your feedback",
+ "response_id": response_id,
+ "rating": rating,
+ }
diff --git a/python/rag/healthcare-support-portal/packages/rag/src/rag_service/routers/documents.py b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/routers/documents.py
new file mode 100644
index 00000000..4f4c96dd
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/routers/documents.py
@@ -0,0 +1,522 @@
+import sys
+from pathlib import Path
+
+# Add the common package to Python path
+common_path = Path(__file__).parent.parent.parent.parent.parent / "common" / "src"
+sys.path.insert(0, str(common_path))
+
+
+from common.auth import get_current_user
+from common.db import get_db
+from common.models import Document, User
+from common.oso_sync import remove_document_access, sync_document_access
+from common.schemas import DocumentCreate, DocumentResponse
+from fastapi import (
+ APIRouter,
+ Depends,
+ File,
+ HTTPException,
+ Query,
+ Request,
+ UploadFile,
+ status,
+)
+from sqlalchemy.orm import Session
+from sqlalchemy_oso_cloud import authorized, get_oso
+
+from ..utils.embeddings import (
+ get_embedding_status,
+ regenerate_document_embeddings,
+ store_document_embeddings,
+)
+from ..utils.text_processing import chunk_text, clean_text
+from ..observability import (
+ embedding_generation_context,
+ log_document_upload,
+ log_embeddings_stored,
+ log_galileo_event,
+ logger,
+)
+
+router = APIRouter()
+
+
+@router.get("/", response_model=list[DocumentResponse])
+async def list_documents(
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+ skip: int = Query(0, ge=0),
+ limit: int = Query(100, ge=1, le=1000),
+ document_type: str | None = Query(None),
+ department: str | None = Query(None),
+):
+ """
+ List documents with Oso authorization filtering
+ """
+ # Use Oso Cloud to filter documents the current user can read
+ # Add error handling for development when OSO is not available
+ try:
+ query = db.query(Document).options(authorized(current_user, "read", Document))
+ except Exception as oso_error:
+ logger.warning("OSO authorization failed, falling back to basic query", error=str(oso_error), user_role=current_user.role)
+ # In development, fallback to showing documents based on role/department
+ query = db.query(Document)
+ if current_user.role != "admin":
+ # Non-admins only see documents from their department
+ query = query.filter(Document.department == current_user.department)
+
+ # Apply optional filters
+ if document_type:
+ query = query.filter(Document.document_type == document_type)
+
+ if department:
+ query = query.filter(Document.department == department)
+
+ # Apply pagination
+ documents = query.offset(skip).limit(limit).all()
+
+ logger.info(
+ "Documents listed",
+ user_role=current_user.role,
+ document_type=document_type,
+ department=department,
+ count=len(documents),
+ skip=skip,
+ limit=limit,
+ )
+
+ return documents
+
+
+@router.get("/embedding-statuses")
+async def get_all_embedding_statuses(
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Get embedding status for all documents the user can access
+ """
+ # Use Oso Cloud to filter documents the current user can read
+ # Add error handling for development when OSO is not available
+ try:
+ authorized_documents = db.query(Document).options(authorized(current_user, "read", Document)).all()
+ except Exception as oso_error:
+ logger.warning(
+ "OSO authorization failed in embedding statuses, falling back to basic query",
+ error=str(oso_error),
+ user_role=current_user.role,
+ )
+ # In development, fallback to showing documents based on role/department
+ if current_user.role == "admin":
+ authorized_documents = db.query(Document).all()
+ else:
+ authorized_documents = db.query(Document).filter(Document.department == current_user.department).all()
+
+ statuses = {}
+ for document in authorized_documents:
+ embedding_status = await get_embedding_status(document.id, db)
+ statuses[document.id] = embedding_status
+
+ logger.info(
+ "Embedding statuses retrieved",
+ user_role=current_user.role,
+ documents_count=len(authorized_documents),
+ statuses_count=len(statuses),
+ )
+
+ return statuses
+
+
+@router.get("/{document_id}", response_model=DocumentResponse)
+async def get_document(
+ document_id: int,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Get specific document with Oso authorization
+ """
+ oso = get_oso()
+
+ # Get the document
+ document = db.query(Document).filter(Document.id == document_id).first()
+
+ if not document:
+ logger.warning("Document not found", document_id=document_id, user_role=current_user.role)
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
+
+ # Check authorization with OSO fallback
+ try:
+ oso.authorize(current_user, "read", document)
+ except Exception as e:
+ logger.warning(
+ "OSO authorization failed for document access, checking basic authorization",
+ document_id=document_id,
+ user_role=current_user.role,
+ error=str(e),
+ )
+ # Fallback authorization logic for development
+ if current_user.role == "admin":
+ # Admins can access any document
+ pass
+ elif current_user.department == document.department:
+ # Users can access documents from their department
+ pass
+ else:
+ # Access denied
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied - document not in your department")
+
+ logger.info(
+ "Document accessed",
+ document_id=document_id,
+ user_role=current_user.role,
+ document_type=document.document_type,
+ department=document.department,
+ )
+
+ return document
+
+
+@router.post("/", response_model=DocumentResponse)
+async def create_document(
+ document_data: DocumentCreate,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Create a new document
+ """
+ # Create document
+ document = Document(
+ title=document_data.title,
+ content=document_data.content,
+ document_type=document_data.document_type,
+ department=document_data.department,
+ is_sensitive=document_data.is_sensitive,
+ created_by_id=current_user.id,
+ )
+
+ db.add(document)
+ db.commit()
+ db.refresh(document)
+
+ # Sync Oso facts
+ sync_document_access(document)
+
+ # Log document creation
+ log_galileo_event(
+ event_type="document_created",
+ event_data={
+ "document_id": document.id,
+ "title": document.title,
+ "document_type": document.document_type,
+ "department": document.department,
+ "is_sensitive": document.is_sensitive,
+ "content_length": len(document.content),
+ },
+ user_id=str(current_user.id),
+ )
+
+ logger.info(
+ "Document created",
+ document_id=document.id,
+ user_role=current_user.role,
+ document_type=document.document_type,
+ department=document.department,
+ )
+
+ return document
+
+
+@router.post("/upload")
+async def upload_document(
+ file: UploadFile = File(...),
+ title: str = None,
+ document_type: str = None,
+ department: str = None,
+ is_sensitive: bool = False,
+ request: Request = None,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Upload and process a document file
+ """
+ settings = request.app.state.settings
+
+ # Read file content
+ content = await file.read()
+ file_size = len(content)
+
+ # Clean and process text
+ text_content = clean_text(content.decode("utf-8"))
+
+ # Use filename as title if not provided
+ if not title:
+ title = file.filename
+
+ # Create document
+ document = Document(
+ title=title,
+ content=text_content,
+ document_type=document_type or "unknown",
+ department=department or current_user.department,
+ is_sensitive=is_sensitive,
+ created_by_id=current_user.id,
+ )
+
+ db.add(document)
+ db.commit()
+ db.refresh(document)
+
+ # Sync Oso facts
+ sync_document_access(document)
+
+ # Log document upload
+ log_document_upload(
+ document_type=document.document_type,
+ department=document.department,
+ file_size=file_size,
+ document_id=document.id,
+ )
+
+ # Generate embeddings with observability
+ try:
+ chunks = chunk_text(
+ text_content,
+ chunk_size=settings.chunk_size,
+ chunk_overlap=settings.chunk_overlap,
+ )
+
+ async with embedding_generation_context(model=settings.embedding_model, chunk_count=len(chunks)) as operation_id:
+
+ success = await store_document_embeddings(
+ document=document,
+ chunks=chunks,
+ db=db,
+ model=settings.embedding_model,
+ )
+
+ if success:
+ log_embeddings_stored(document_id=document.id, chunk_count=len(chunks))
+
+ # Log to Galileo
+ log_galileo_event(
+ event_type="document_uploaded_with_embeddings",
+ event_data={
+ "document_id": document.id,
+ "title": document.title,
+ "document_type": document.document_type,
+ "department": document.department,
+ "file_size": file_size,
+ "chunks_count": len(chunks),
+ "embedding_model": settings.embedding_model,
+ "operation_id": operation_id,
+ },
+ user_id=str(current_user.id),
+ )
+
+ logger.info(
+ "Document uploaded and embeddings generated successfully",
+ document_id=document.id,
+ user_role=current_user.role,
+ chunks_count=len(chunks),
+ operation_id=operation_id,
+ )
+
+ return {
+ "message": "Document uploaded and processed successfully",
+ "document_id": document.id,
+ "chunks_created": len(chunks),
+ "embedding_status": "completed",
+ }
+ else:
+ logger.error(
+ "Failed to generate embeddings for uploaded document",
+ document_id=document.id,
+ user_role=current_user.role,
+ )
+
+ return {
+ "message": "Document uploaded but embedding generation failed",
+ "document_id": document.id,
+ "embedding_status": "failed",
+ }
+
+ except Exception as e:
+ logger.error("Error processing uploaded document", document_id=document.id, user_role=current_user.role, error=str(e))
+
+ # Log error to Galileo
+ log_galileo_event(
+ event_type="document_upload_error",
+ event_data={
+ "document_id": document.id,
+ "title": document.title,
+ "error": str(e),
+ "error_type": type(e).__name__,
+ },
+ user_id=str(current_user.id),
+ )
+
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error processing document: {str(e)}",
+ )
+
+
+@router.post("/{document_id}/regenerate-embeddings")
+async def regenerate_embeddings(
+ document_id: int,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Regenerate embeddings for a document
+ """
+ settings = request.app.state.settings
+
+ # Get document
+ document = db.query(Document).filter(Document.id == document_id).first()
+
+ if not document:
+ logger.warning("Document not found for embedding regeneration", document_id=document_id, user_role=current_user.role)
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
+
+ # Check authorization
+ try:
+ oso = get_oso()
+ oso.authorize(current_user, "read", document)
+ except Exception as e:
+ logger.warning(
+ "Unauthorized embedding regeneration attempt",
+ document_id=document_id,
+ user_role=current_user.role,
+ error=str(e),
+ )
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
+
+ # Regenerate embeddings with observability
+ try:
+ chunks = chunk_text(
+ document.content,
+ chunk_size=settings.chunk_size,
+ chunk_overlap=settings.chunk_overlap,
+ )
+
+ async with embedding_generation_context(model=settings.embedding_model, chunk_count=len(chunks)) as operation_id:
+
+ result = await regenerate_document_embeddings(
+ document=document,
+ db=db,
+ model=settings.embedding_model,
+ )
+
+ if result["success"]:
+ log_embeddings_stored(document_id=document.id, chunk_count=len(chunks))
+
+ # Log to Galileo
+ log_galileo_event(
+ event_type="embeddings_regenerated",
+ event_data={
+ "document_id": document.id,
+ "title": document.title,
+ "chunks_count": len(chunks),
+ "embedding_model": settings.embedding_model,
+ "operation_id": operation_id,
+ },
+ user_id=str(current_user.id),
+ )
+
+ logger.info(
+ "Document embeddings regenerated successfully",
+ document_id=document.id,
+ user_role=current_user.role,
+ chunks_count=len(chunks),
+ operation_id=operation_id,
+ )
+
+ return result
+ else:
+ logger.error(
+ "Failed to regenerate document embeddings",
+ document_id=document.id,
+ user_role=current_user.role,
+ error=result["message"],
+ )
+
+ return result
+
+ except Exception as e:
+ logger.error("Error regenerating document embeddings", document_id=document_id, user_role=current_user.role, error=str(e))
+
+ # Log error to Galileo
+ log_galileo_event(
+ event_type="embedding_regeneration_error",
+ event_data={"document_id": document_id, "error": str(e), "error_type": type(e).__name__},
+ user_id=str(current_user.id),
+ )
+
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error regenerating embeddings: {str(e)}",
+ )
+
+
+@router.delete("/{document_id}")
+async def delete_document(
+ document_id: int,
+ request: Request,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ """
+ Delete a document
+ """
+ # Get document
+ document = db.query(Document).filter(Document.id == document_id).first()
+
+ if not document:
+ logger.warning("Document not found for deletion", document_id=document_id, user_role=current_user.role)
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
+
+ # Check authorization
+ try:
+ oso = get_oso()
+ oso.authorize(current_user, "delete", document)
+ except Exception as e:
+ logger.warning("Unauthorized document deletion attempt", document_id=document_id, user_role=current_user.role, error=str(e))
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
+
+ # Remove Oso facts
+ remove_document_access(document)
+
+ # Delete document
+ db.delete(document)
+ db.commit()
+
+ # Log to Galileo
+ log_galileo_event(
+ event_type="document_deleted",
+ event_data={
+ "document_id": document_id,
+ "title": document.title,
+ "document_type": document.document_type,
+ "department": document.department,
+ },
+ user_id=str(current_user.id),
+ )
+
+ logger.info(
+ "Document deleted",
+ document_id=document_id,
+ user_role=current_user.role,
+ document_type=document.document_type,
+ department=document.department,
+ )
+
+ return {"message": "Document deleted successfully"}
diff --git a/python/rag/healthcare-support-portal/packages/rag/src/rag_service/utils/__init__.py b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/utils/__init__.py
new file mode 100644
index 00000000..84095a64
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/utils/__init__.py
@@ -0,0 +1 @@
+# Utils package initialization
diff --git a/python/rag/healthcare-support-portal/packages/rag/src/rag_service/utils/embeddings.py b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/utils/embeddings.py
new file mode 100644
index 00000000..53f08872
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/utils/embeddings.py
@@ -0,0 +1,265 @@
+import sys
+from pathlib import Path
+
+# Add the common package to Python path
+common_path = Path(__file__).parent.parent.parent.parent.parent / "common" / "src"
+sys.path.insert(0, str(common_path))
+
+from galileo.openai import openai
+from common.models import Document, Embedding
+from common.oso_sync import sync_embedding_access
+from sqlalchemy import text
+from sqlalchemy.orm import Session
+
+
+async def generate_embedding(text: str, model: str = "text-embedding-3-small") -> list[float]:
+ """Generate embedding for a given text using OpenAI (Galileo instrumented)."""
+ try:
+ # Import settings to get API key
+ from ..config import settings
+
+ client = openai.OpenAI(api_key=settings.openai_api_key)
+ response = client.embeddings.create(input=text, model=model)
+ return response.data[0].embedding
+ except Exception as e:
+ print(f"Error generating embedding: {e}")
+ return []
+
+
+async def store_document_embeddings(
+ document: Document,
+ chunks: list[str],
+ db: Session,
+ model: str = "text-embedding-3-small",
+) -> bool:
+ """Generate and store embeddings for document chunks."""
+ try:
+ for i, chunk in enumerate(chunks):
+ # Generate embedding
+ embedding_vector = await generate_embedding(chunk, model)
+
+ if not embedding_vector:
+ continue
+
+ # Create embedding record
+ db_embedding = Embedding(
+ document_id=document.id,
+ content_chunk=chunk,
+ embedding_vector=embedding_vector,
+ chunk_index=i,
+ )
+
+ db.add(db_embedding)
+ db.commit()
+ db.refresh(db_embedding)
+
+ # Sync OSO facts for new embedding
+ try:
+ sync_embedding_access(db_embedding)
+ except Exception as e:
+ print(f"Warning: Failed to sync OSO facts for embedding {db_embedding.id}: {e}")
+
+ return True
+
+ except Exception as e:
+ print(f"Error storing embeddings: {e}")
+ db.rollback()
+ return False
+
+
+async def similarity_search(
+ query_text: str,
+ db: Session,
+ limit: int = 5,
+ similarity_threshold: float = 0.1,
+ document_ids: list[int] | None = None,
+) -> list[dict]:
+ """
+ Perform similarity search using pgvector.
+ """
+ import time
+
+ start_time = time.time()
+
+ try:
+ # Generate query embedding
+ query_embedding = await generate_embedding(query_text)
+
+ if not query_embedding:
+ return []
+
+ # Build SQL query for similarity search
+ base_query = """
+ SELECT
+ e.id,
+ e.document_id,
+ e.content_chunk,
+ e.chunk_index,
+ d.title,
+ d.document_type,
+ d.department,
+ d.is_sensitive,
+ 1 - (e.embedding_vector <=> :query_vector) as similarity
+ FROM embeddings e
+ JOIN documents d ON e.document_id = d.id
+ WHERE 1 - (e.embedding_vector <=> :query_vector) > :threshold
+ """
+
+ # Convert query embedding to proper format for pgvector
+ params = {
+ "query_vector": str(query_embedding),
+ "threshold": similarity_threshold,
+ }
+
+ # Add document ID filter if provided
+ if document_ids:
+ placeholders = ",".join([f":doc_id_{i}" for i in range(len(document_ids))])
+ base_query += f" AND e.document_id IN ({placeholders})"
+ for i, doc_id in enumerate(document_ids):
+ params[f"doc_id_{i}"] = doc_id
+
+ base_query += " ORDER BY similarity DESC LIMIT :limit"
+ params["limit"] = limit
+
+ # Execute query with proper parameter binding
+ result = db.execute(text(base_query), params)
+ rows = result.fetchall()
+
+ # Convert to list of dictionaries
+ results = []
+ for row in rows:
+ results.append(
+ {
+ # Frontend-compatible field names
+ "id": row[1], # document_id → id
+ "title": row[4], # document_title → title
+ "content_chunk": row[2],
+ "document_type": row[5],
+ "department": row[6],
+ "similarity_score": float(row[8]), # similarity → similarity_score
+ "created_at": "", # Will be populated by document data if needed
+ # Keep internal fields for backend processing
+ "embedding_id": row[0],
+ "document_id": row[1], # Keep for internal use
+ "chunk_index": row[3],
+ "document_title": row[4], # Keep for internal use
+ "is_sensitive": row[7],
+ "similarity": float(row[8]), # Keep for internal use
+ }
+ )
+
+ # Log retriever call to Galileo
+ try:
+ from ..observability import log_retriever_call
+
+ duration_ns = int((time.time() - start_time) * 1_000_000_000)
+
+ # Format results for Galileo logging
+ formatted_results = []
+ for result in results:
+ formatted_results.append(
+ {
+ "document_title": result["title"],
+ "content_chunk": (result["content_chunk"][:200] + "..." if len(result["content_chunk"]) > 200 else result["content_chunk"]),
+ "similarity_score": result["similarity_score"],
+ "document_type": result["document_type"],
+ "department": result["department"],
+ }
+ )
+
+ log_retriever_call(query=query_text, documents=formatted_results, duration_ns=duration_ns)
+ except Exception as log_error:
+ print(f"Warning: Failed to log retriever call to Galileo: {log_error}")
+
+ return results
+
+ except Exception as e:
+ print(f"Error in similarity search: {e}")
+ return []
+
+
+async def regenerate_document_embeddings(document: Document, db: Session, model: str = "text-embedding-3-small") -> dict:
+ """
+ Regenerate embeddings for an existing document.
+ Returns status dict with success and message.
+ """
+ try:
+ from ..utils.text_processing import chunk_text
+
+ # Delete existing embeddings for this document
+ db.query(Embedding).filter(Embedding.document_id == document.id).delete()
+ db.commit()
+
+ # Create new chunks from document content
+ chunks = chunk_text(
+ document.content,
+ chunk_size=500, # Default chunk size
+ chunk_overlap=50, # Default overlap
+ )
+
+ # Generate and store new embeddings
+ success = await store_document_embeddings(document, chunks, db, model)
+
+ if success:
+ return {
+ "success": True,
+ "message": f"Successfully regenerated {len(chunks)} embeddings for document {document.id}",
+ "chunks_created": len(chunks),
+ }
+ else:
+ return {
+ "success": False,
+ "message": f"Failed to regenerate embeddings for document {document.id}",
+ "chunks_created": 0,
+ }
+
+ except Exception as e:
+ print(f"Error regenerating embeddings for document {document.id}: {e}")
+ db.rollback()
+ return {"success": False, "message": f"Error: {str(e)}", "chunks_created": 0}
+
+
+async def get_embedding_status(document_id: int, db: Session) -> dict:
+ """
+ Get embedding status for a document.
+ """
+ try:
+ embedding_count = db.query(Embedding).filter(Embedding.document_id == document_id).count()
+
+ return {
+ "document_id": document_id,
+ "has_embeddings": embedding_count > 0,
+ "embedding_count": embedding_count,
+ }
+ except Exception as e:
+ print(f"Error getting embedding status for document {document_id}: {e}")
+ return {
+ "document_id": document_id,
+ "has_embeddings": False,
+ "embedding_count": 0,
+ "error": str(e),
+ }
+
+
+def combine_chunks_for_context(search_results: list[dict], max_tokens: int = 6000) -> str:
+ """
+ Combine relevant chunks into context for RAG, respecting token limits.
+ """
+ context_parts = []
+ current_tokens = 0
+
+ for result in search_results:
+ chunk = result["content_chunk"]
+ title = result["document_title"]
+
+ # Simple token estimation (more accurate would use tiktoken)
+ chunk_tokens = len(chunk.split()) * 1.3 # Rough estimation
+
+ if current_tokens + chunk_tokens > max_tokens:
+ break
+
+ context_part = f"Document: {title}\nContent: {chunk}\n---\n"
+ context_parts.append(context_part)
+ current_tokens += chunk_tokens
+
+ return "\n".join(context_parts)
diff --git a/python/rag/healthcare-support-portal/packages/rag/src/rag_service/utils/text_processing.py b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/utils/text_processing.py
new file mode 100644
index 00000000..9ad896fd
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/src/rag_service/utils/text_processing.py
@@ -0,0 +1,72 @@
+import re
+
+import tiktoken
+
+
+def clean_text(text: str) -> str:
+ """Clean and normalize text content."""
+ # Remove extra whitespace
+ text = re.sub(r"\s+", " ", text)
+ # Remove special characters but keep medical terminology
+ text = re.sub(r"[^\w\s\-\.\,\:\;\(\)\/]", "", text)
+ return text.strip()
+
+
+def chunk_text(text: str, chunk_size: int = 1000, chunk_overlap: int = 200) -> list[str]:
+ """
+ Split text into overlapping chunks for better context preservation.
+ """
+ # Initialize tokenizer
+ encoding = tiktoken.get_encoding("cl100k_base")
+
+ # Tokenize the text
+ tokens = encoding.encode(text)
+
+ chunks = []
+ start = 0
+
+ while start < len(tokens):
+ # Define the end of the current chunk
+ end = start + chunk_size
+
+ # Extract chunk tokens
+ chunk_tokens = tokens[start:end]
+
+ # Decode back to text
+ chunk_text = encoding.decode(chunk_tokens)
+
+ chunks.append(chunk_text.strip())
+
+ # Move start position with overlap
+ start = end - chunk_overlap
+
+ # Break if we've processed all tokens
+ if end >= len(tokens):
+ break
+
+ return chunks
+
+
+def extract_keywords(text: str) -> list[str]:
+ """Extract potential medical keywords from text."""
+ # Simple keyword extraction - in production, use more sophisticated NLP
+ medical_patterns = [
+ r"\b[A-Z][a-z]+\s+[A-Z][a-z]+\b", # Proper nouns (conditions, medications)
+ r"\b\d+\s*mg\b", # Dosages
+ r"\b\d+\s*ml\b", # Volumes
+ r"\bICD[-\s]?\d+\b", # ICD codes
+ r"\bCPT[-\s]?\d+\b", # CPT codes
+ ]
+
+ keywords = []
+ for pattern in medical_patterns:
+ matches = re.findall(pattern, text, re.IGNORECASE)
+ keywords.extend(matches)
+
+ return list(set(keywords)) # Remove duplicates
+
+
+def calculate_token_count(text: str) -> int:
+ """Calculate the number of tokens in a text."""
+ encoding = tiktoken.get_encoding("cl100k_base")
+ return len(encoding.encode(text))
diff --git a/python/rag/healthcare-support-portal/packages/rag/test_config.py b/python/rag/healthcare-support-portal/packages/rag/test_config.py
new file mode 100644
index 00000000..66ff1cee
--- /dev/null
+++ b/python/rag/healthcare-support-portal/packages/rag/test_config.py
@@ -0,0 +1,226 @@
+#!/usr/bin/env python3
+"""
+Configuration Test Script for Healthcare RAG Service
+
+This script tests that all required environment variables and API keys
+are properly configured for the RAG service.
+
+Usage:
+ uv run python test_config.py
+"""
+
+import os
+import sys
+from typing import Dict, Any
+
+from dotenv import load_dotenv
+
+load_dotenv()
+
+
+def test_environment_variables() -> Dict[str, Any]:
+ """Test that all required environment variables are set."""
+ results = {}
+
+ # Required variables
+ required_vars = {
+ "DATABASE_URL": "PostgreSQL database connection string",
+ "SECRET_KEY": "JWT signing key",
+ "OPENAI_API_KEY": "OpenAI API key for embeddings and chat",
+ }
+
+ # Optional variables with defaults
+ optional_vars = {
+ "DEBUG": "Debug mode (default: false)",
+ "EMBEDDING_MODEL": "OpenAI embedding model (default: text-embedding-3-small)",
+ "CHAT_MODEL": "OpenAI chat model (default: gpt-4o-mini)",
+ "GALILEO_API_KEY": "Galileo API key for observability",
+ "GALILEO_PROJECT_NAME": "Galileo project name (default: healthcare-rag)",
+ "GALILEO_ENVIRONMENT": "Galileo environment tag (default: development)",
+ "CHUNK_SIZE": "Document chunk size (default: 1000)",
+ "SIMILARITY_THRESHOLD": "Vector search threshold (default: 0.7)",
+ }
+
+ print("🔍 Testing Environment Variables...")
+ print("=" * 50)
+
+ # Test required variables
+ all_required_present = True
+ for var, description in required_vars.items():
+ value = os.getenv(var)
+ if value:
+ if var == "OPENAI_API_KEY":
+ # Don't expose the full key
+ display_value = f"{value[:8]}...{value[-4:]}" if len(value) > 12 else "[HIDDEN]"
+ else:
+ display_value = f"{value[:20]}..." if len(value) > 20 else value
+ print(f"✅ {var}: {display_value}")
+ results[var] = {"status": "OK", "value": value}
+ else:
+ print(f"❌ {var}: NOT SET ({description})")
+ results[var] = {"status": "MISSING", "value": None}
+ all_required_present = False
+
+ print()
+
+ # Test optional variables
+ for var, description in optional_vars.items():
+ value = os.getenv(var)
+ if value:
+ if "API_KEY" in var:
+ display_value = f"{value[:8]}...{value[-4:]}" if len(value) > 12 else "[HIDDEN]"
+ else:
+ display_value = value
+ print(f"🟡 {var}: {display_value}")
+ results[var] = {"status": "SET", "value": value}
+ else:
+ print(f"⚪ {var}: Not set ({description})")
+ results[var] = {"status": "DEFAULT", "value": None}
+
+ results["all_required_present"] = all_required_present
+ return results
+
+
+def test_openai_connection() -> bool:
+ """Test OpenAI API connection."""
+ try:
+ import openai
+ from openai import OpenAI
+
+ api_key = os.getenv("OPENAI_API_KEY")
+ if not api_key:
+ print("❌ OpenAI API key not found")
+ return False
+
+ if not api_key.startswith("sk-"):
+ print(f"❌ OpenAI API key format invalid (should start with 'sk-'): {api_key[:10]}...")
+ return False
+
+ print("⚙️ Testing OpenAI API connection...")
+ client = OpenAI(api_key=api_key)
+
+ # Test with a simple API call
+ models = client.models.list()
+ print(f"✅ OpenAI API connection successful ({len(models.data)} models available)")
+ return True
+
+ except ImportError:
+ print("❌ OpenAI package not installed. Run: uv add openai")
+ return False
+ except Exception as e:
+ print(f"❌ OpenAI API connection failed: {str(e)}")
+ return False
+
+
+def test_database_connection() -> bool:
+ """Test database connection."""
+ try:
+ from sqlalchemy import create_engine, text
+
+ db_url = os.getenv("DATABASE_URL")
+ if not db_url:
+ print("❌ DATABASE_URL not found")
+ return False
+
+ print("⚙️ Testing database connection...")
+ engine = create_engine(db_url)
+
+ with engine.connect() as conn:
+ result = conn.execute(text("SELECT version()"))
+ version = result.fetchone()[0]
+ print(f"✅ Database connection successful")
+ print(f" PostgreSQL version: {version.split(',')[0]}")
+
+ # Check for pgvector extension
+ result = conn.execute(text("SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector')"))
+ has_vector = result.fetchone()[0]
+ if has_vector:
+ print(f"✅ pgvector extension is installed")
+ else:
+ print(f"⚠️ pgvector extension not found (required for embeddings)")
+
+ return True
+
+ except ImportError as e:
+ print(f"❌ Database packages not installed: {e}")
+ return False
+ except Exception as e:
+ print(f"❌ Database connection failed: {str(e)}")
+ return False
+
+
+def test_galileo_connection() -> bool:
+ """Test Galileo API connection."""
+ try:
+ api_key = os.getenv("GALILEO_API_KEY")
+ if not api_key or api_key == "your-galileo-api-key-here":
+ print("⚠️ Galileo API key not configured (using default placeholder)")
+ print(" Get your API key from: https://app.galileo.ai/sign-up")
+ return True # Don't fail the config test, just warn
+
+ # For now, just check that the key exists
+ # A full test would require the galileo package and network call
+ print(f"✅ Galileo API key configured ({api_key[:8]}...)")
+
+ project = os.getenv("GALILEO_PROJECT_NAME", "healthcare-rag")
+ environment = os.getenv("GALILEO_ENVIRONMENT", "development")
+ print(f" Project: {project}")
+ print(f" Environment: {environment}")
+
+ return True
+
+ except Exception as e:
+ print(f"❌ Galileo configuration error: {str(e)}")
+ return False
+
+
+def main():
+ """Run all configuration tests."""
+ print("🏥 Healthcare RAG Configuration Test")
+ print("=" * 50)
+ print()
+
+ # Test environment variables
+ env_results = test_environment_variables()
+ env_ok = env_results["all_required_present"]
+ print()
+
+ # Test connections
+ openai_ok = test_openai_connection()
+ print()
+
+ db_ok = test_database_connection()
+ print()
+
+ galileo_ok = test_galileo_connection()
+ print()
+
+ # Summary
+ print("=" * 50)
+ print("📊 Configuration Test Summary:")
+ print(f" Environment Variables: {'✅ PASS' if env_ok else '❌ FAIL'}")
+ print(f" OpenAI Connection: {'✅ PASS' if openai_ok else '❌ FAIL'}")
+ print(f" Database Connection: {'✅ PASS' if db_ok else '❌ FAIL'}")
+ print(f" Galileo Configuration: {'✅ PASS' if galileo_ok else '❌ FAIL'}")
+ print()
+
+ if all([env_ok, openai_ok, db_ok, galileo_ok]):
+ print("🎉 All tests passed! RAG service is ready to run.")
+ sys.exit(0)
+ else:
+ print("⚠️ Some tests failed. Please fix the issues above before running the RAG service.")
+ print()
+ print("🔧 Quick fixes:")
+ if not env_ok:
+ print(" - Copy .env.example to .env and fill in your API keys")
+ if not openai_ok:
+ print(" - Get your OpenAI API key from https://platform.openai.com/api-keys")
+ if not db_ok:
+ print(" - Make sure PostgreSQL is running with: docker-compose up -d db")
+ if not galileo_ok:
+ print(" - Set GALILEO_ENABLED=false or add your Galileo API key")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/python/rag/healthcare-support-portal/pyproject.toml b/python/rag/healthcare-support-portal/pyproject.toml
new file mode 100644
index 00000000..785a1629
--- /dev/null
+++ b/python/rag/healthcare-support-portal/pyproject.toml
@@ -0,0 +1,82 @@
+[project]
+name = "healthcare-support-portal"
+version = "0.1.0"
+description = "Healthcare Support Portal - Microservices RAG Application"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+ "bcrypt>=4.0.0,<5.0.0",
+ "oso-cloud>=2.5.0",
+ "psycopg2-binary>=2.9.10",
+ "python-multipart>=0.0.20",
+ "sqlalchemy-oso-cloud>=0.1.0",
+]
+
+[tool.uv.workspace]
+members = [
+ "packages/common",
+ "packages/auth",
+ "packages/patient",
+ "packages/rag"
+]
+
+[dependency-groups]
+dev = [
+ "ruff>=0.12.8",
+]
+
+[tool.ruff]
+# Exclude a variety of commonly ignored directories.
+exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".hg",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "build",
+ "dist",
+ "node_modules",
+ "venv",
+ "frontend/node_modules",
+]
+
+# Same as Black.
+line-length = 88
+
+# Target Python 3.11+.
+target-version = "py311"
+
+[tool.ruff.lint]
+# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
+select = ["E4", "E7", "E9", "F", "I", "UP", "B", "C4", "PIE", "SIM"]
+ignore = ["B008", "E402", "B904"] # B008: FastAPI Depends pattern; E402: imports after path manipulation; B904: Exception chaining not needed for HTTP errors
+
+# Allow fix for all enabled rules (when `--fix`) is provided.
+fixable = ["ALL"]
+unfixable = []
+
+# Allow unused variables when underscore-prefixed.
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+
+[tool.ruff.format]
+# Like Black, use double quotes for strings.
+quote-style = "double"
+
+# Like Black, indent with spaces, rather than tabs.
+indent-style = "space"
+
+# Like Black, respect magic trailing commas.
+skip-magic-trailing-comma = false
+
+# Like Black, automatically detect the appropriate line ending.
+line-ending = "auto"
diff --git a/python/rag/healthcare-support-portal/run_all.sh b/python/rag/healthcare-support-portal/run_all.sh
new file mode 100755
index 00000000..934993ff
--- /dev/null
+++ b/python/rag/healthcare-support-portal/run_all.sh
@@ -0,0 +1,207 @@
+#!/bin/bash
+# run_all.sh - Start all Healthcare Support Portal services and infrastructure
+
+# Exit on any error (but allow controlled errors)
+set -e
+
+# Function to check if a command exists
+command_exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+echo "🏥 Healthcare Support Portal - Starting All Services"
+echo "=================================================="
+
+# Function to check if a port is available
+check_port() {
+ local port=$1
+ if lsof -i :$port -sTCP:LISTEN -t >/dev/null 2>&1; then
+ echo "❌ Port $port is already in use"
+ return 1
+ else
+ echo "✅ Port $port is available"
+ return 0
+ fi
+}
+
+# Check if required ports are available
+echo "🔍 Checking port availability..."
+check_port 8001 || exit 1
+check_port 8002 || exit 1
+check_port 8003 || exit 1
+check_port 3000 || exit 1
+
+# Special handling for port 8080 (Oso Dev Server) - informational only
+if lsof -i :8080 -sTCP:LISTEN -t >/dev/null 2>&1; then
+ echo "ℹ️ Port 8080 is in use (Oso Dev Server likely running)"
+else
+ echo "✅ Port 8080 is available"
+fi
+
+# Check prerequisites before starting services
+echo "🔍 Checking prerequisites..."
+
+# Check Docker
+if ! command_exists docker; then
+ echo "❌ Docker not found. Please install Docker and try again."
+ exit 1
+fi
+
+# Check docker-compose
+if ! command_exists docker-compose && ! docker compose version >/dev/null 2>&1; then
+ echo "❌ Docker Compose not found. Please install Docker Compose and try again."
+ exit 1
+fi
+
+# Check if frontend dependencies are installed
+echo "🔍 Checking frontend dependencies..."
+if [ ! -d "frontend/node_modules" ]; then
+ echo "⚠️ Frontend dependencies not found. Installing..."
+ if command_exists node && command_exists npm; then
+ NODE_VERSION=$(node --version | sed 's/v//')
+ if [ "$(printf '%s\n' "20.19.0" "$NODE_VERSION" | sort -V | head -n1)" = "20.19.0" ]; then
+ echo "📦 Installing frontend dependencies..."
+ cd frontend
+ if ! npm install; then
+ echo "❌ Failed to install frontend dependencies"
+ cd ..
+ exit 1
+ fi
+ cd ..
+ echo "✅ Frontend dependencies installed"
+ else
+ echo "❌ Node.js $NODE_VERSION found, but 20.19.0+ is required for frontend"
+ echo " Frontend service will not start. Please upgrade Node.js."
+ SKIP_FRONTEND=true
+ fi
+ else
+ echo "❌ Node.js or npm not found. Frontend service will not start."
+ echo " Please install Node.js 20.19.0+ and run: cd frontend && npm install"
+ SKIP_FRONTEND=true
+ fi
+else
+ echo "✅ Frontend dependencies found"
+fi
+
+echo ""
+echo "🚀 Starting services..."
+
+# Start infrastructure services if not running
+echo "🏗️ Checking infrastructure services..."
+if ! docker ps | grep -q healthcare-support-portal; then
+ echo "Starting PostgreSQL database and Oso Dev Server..."
+ docker-compose up -d db oso
+ echo "Waiting for database to be ready..."
+ sleep 8
+ echo "Running database migrations..."
+ docker-compose run --rm migrate
+ echo "✅ Database migrations completed"
+else
+ echo "✅ Infrastructure services (PostgreSQL + Oso Dev Server) are already running"
+ echo "🔍 Checking if migrations are current..."
+ docker-compose run --rm migrate
+fi
+
+# Create logs directory if it doesn't exist
+echo "📁 Creating logs directory..."
+if ! mkdir -p logs 2>/dev/null; then
+ echo "⚠️ Warning: Could not create logs directory. Output will go to console."
+ LOG_TO_FILE=false
+else
+ LOG_TO_FILE=true
+fi
+
+# Get absolute path for logs
+ROOT_DIR=$(pwd)
+
+# Start services in background using subshells to avoid directory changes
+echo "🔐 Starting Auth Service..."
+if [ "$LOG_TO_FILE" = true ]; then
+ (cd packages/auth && ./run.sh) > "$ROOT_DIR/logs/auth.log" 2>&1 &
+else
+ (cd packages/auth && ./run.sh) &
+fi
+AUTH_PID=$!
+
+echo "🏥 Starting Patient Service..."
+if [ "$LOG_TO_FILE" = true ]; then
+ (cd packages/patient && ./run.sh) > "$ROOT_DIR/logs/patient.log" 2>&1 &
+else
+ (cd packages/patient && ./run.sh) &
+fi
+PATIENT_PID=$!
+
+echo "🤖 Starting RAG Service..."
+if [ "$LOG_TO_FILE" = true ]; then
+ (cd packages/rag && ./run.sh) > "$ROOT_DIR/logs/rag.log" 2>&1 &
+else
+ (cd packages/rag && ./run.sh) &
+fi
+RAG_PID=$!
+
+# Start Frontend Service (only if dependencies are available)
+if [ "$SKIP_FRONTEND" = true ]; then
+ echo "⚠️ Skipping Frontend Service (dependencies not available)"
+ FRONTEND_PID=""
+else
+ echo "🌐 Starting Frontend Service..."
+ if [ "$LOG_TO_FILE" = true ]; then
+ (cd frontend && ./run.sh) > "$ROOT_DIR/logs/frontend.log" 2>&1 &
+ else
+ (cd frontend && ./run.sh) &
+ fi
+ FRONTEND_PID=$!
+fi
+
+echo ""
+echo "✅ Services started!"
+echo "=================================================="
+if [ "$SKIP_FRONTEND" != true ]; then
+ echo "🌐 Frontend: http://localhost:3000"
+else
+ echo "⚠️ Frontend: Not started (missing dependencies)"
+fi
+echo "🔐 Auth Service: http://localhost:8001/docs"
+echo "🏥 Patient Service: http://localhost:8002/docs"
+echo "🤖 RAG Service: http://localhost:8003/docs"
+echo "⚖️ Oso Dev Server: http://localhost:8080"
+echo ""
+echo "📋 Service PIDs:"
+if [ -n "$FRONTEND_PID" ]; then
+ echo " Frontend Service: $FRONTEND_PID"
+else
+ echo " Frontend Service: Not started"
+fi
+echo " Auth Service: $AUTH_PID"
+echo " Patient Service: $PATIENT_PID"
+echo " RAG Service: $RAG_PID"
+if [ "$LOG_TO_FILE" = true ]; then
+ echo "📄 Logs are available in the logs/ directory"
+else
+ echo "📄 Service output is displayed in console"
+fi
+echo "🛑 To stop all services, run: ./stop_all.sh"
+echo ""
+
+# Save PIDs for stopping later (create logs directory if needed for PID files)
+if [ "$LOG_TO_FILE" = true ] || mkdir -p logs 2>/dev/null; then
+ if [ -n "$FRONTEND_PID" ]; then
+ echo "$FRONTEND_PID" > logs/frontend.pid
+ fi
+ echo "$AUTH_PID" > logs/auth.pid
+ echo "$PATIENT_PID" > logs/patient.pid
+ echo "$RAG_PID" > logs/rag.pid
+else
+ echo "⚠️ Warning: Could not save PID files. You'll need to stop services manually."
+fi
+
+# Wait for user input
+echo "Press Ctrl+C to stop all services..."
+PIDS_TO_KILL="$AUTH_PID $PATIENT_PID $RAG_PID"
+if [ -n "$FRONTEND_PID" ]; then
+ PIDS_TO_KILL="$FRONTEND_PID $PIDS_TO_KILL"
+fi
+trap "echo ''; echo '🛑 Stopping all services...'; kill $PIDS_TO_KILL 2>/dev/null; exit 0" INT
+
+# Keep script running
+wait
diff --git a/python/rag/healthcare-support-portal/sample-documents/medical-history-derek-shepherd.md b/python/rag/healthcare-support-portal/sample-documents/medical-history-derek-shepherd.md
new file mode 100644
index 00000000..46a2898d
--- /dev/null
+++ b/python/rag/healthcare-support-portal/sample-documents/medical-history-derek-shepherd.md
@@ -0,0 +1,99 @@
+# Patient History Report
+
+**Patient Name:** Dr. Derek Christopher Shepherd
+**DOB:** 1966
+**Age:** 49 (at time of death, 2015)
+**Sex:** Male
+**Occupation:** Neurosurgeon (Attending, later Chief of Neurosurgery at Grey Sloan Memorial Hospital)
+**Date of Report:** Historical summary
+
+---
+
+## Chief Complaint
+No active complaints documented at time of final hospitalization.
+Final medical event: severe traumatic brain injury (TBI) following motor vehicle collision.
+
+---
+
+## History of Present Illness (HPI)
+Dr. Shepherd was involved in a high-speed car accident in 2015. He sustained significant head trauma and other injuries. Despite being conscious at the scene and initially assisting other victims, his own injuries rapidly worsened. He was transported to a local hospital without a Level I trauma center. Delay in proper neurosurgical intervention led to brain death.
+
+---
+
+## Past Medical History
+- **Gunshot wound (shoulder)** – sustained during a hostage situation at Seattle Grace Hospital. Fully recovered.
+- **Hand injury** – loss of fine motor control after a plane crash; temporary impairment, recovered after surgery and rehab.
+- **Chronic stress and insomnia** – secondary to occupational strain.
+- **No chronic conditions** documented (no diabetes, hypertension, or cardiovascular disease).
+
+---
+
+## Surgical History
+- Orthopedic repair of hand injury following plane crash (2012).
+- Minor wound management for gunshot injury.
+- No major elective surgeries.
+
+---
+
+## Medications
+- None documented for chronic use.
+- Occasional post-operative pain management (NSAIDs, opioids short-term).
+
+---
+
+## Allergies
+- No known drug allergies (NKDA).
+
+---
+
+## Family History
+- Mother: deceased.
+- Father: deceased, history of abusive behavior.
+- Sisters: Amelia Shepherd (neurosurgeon), Nancy Shepherd (OB/GYN), Kathleen Shepherd (psychiatrist), Liz Shepherd (immunologist).
+- Notable family history of substance use disorder (Amelia, opioid dependency).
+
+---
+
+## Social History
+- **Tobacco:** Denies use.
+- **Alcohol:** Moderate consumption, no history of abuse.
+- **Drugs:** Denies use.
+- **Marital Status:** Married to Dr. Meredith Grey (General Surgery).
+- **Children:** Zola (adopted), Bailey, and Ellis.
+- **Support System:** Close colleagues at Grey Sloan Memorial Hospital, particularly Dr. Miranda Bailey, Dr. Richard Webber, and Dr. Callie Torres.
+
+---
+
+## Review of Systems
+- **Neurological:** Prior transient hand motor deficits.
+- **Musculoskeletal:** Prior plane crash injuries, resolved.
+- **Psychiatric:** No formal diagnosis, though high occupational stress.
+- **Other systems:** Non-contributory.
+
+---
+
+## Physical Exam (Final Admission)
+- **General:** Male, middle-aged, unconscious post-trauma.
+- **Neuro:** Glasgow Coma Scale 3 upon deterioration; CT revealed severe brain swelling.
+- **Respiratory/Cardiac:** Initially stable, deteriorated rapidly.
+- **Extremities:** Multiple superficial injuries, none life-threatening.
+
+---
+
+## Assessment
+1. Severe traumatic brain injury with herniation.
+2. Status post motor vehicle collision.
+3. Past history of orthopedic and neurological injuries (resolved).
+
+---
+
+## Plan
+- Emergent neurosurgical evaluation was indicated but delayed.
+- Intubation and mechanical ventilation initiated.
+- Supportive care provided until declaration of brain death.
+- Organs donated per patient’s wishes.
+
+---
+
+**Physician’s Note:**
+Dr. Derek Shepherd’s death underscores the importance of timely trauma protocols and neurosurgical intervention in TBI cases. Despite a history of resilience through multiple traumatic injuries, the final event was unsurvivable due to systemic delays in care.
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/sample-documents/medical-history-gregory-house.md b/python/rag/healthcare-support-portal/sample-documents/medical-history-gregory-house.md
new file mode 100644
index 00000000..c05b1584
--- /dev/null
+++ b/python/rag/healthcare-support-portal/sample-documents/medical-history-gregory-house.md
@@ -0,0 +1,100 @@
+# Patient History Report
+
+**Patient Name:** Dr. Gregory House
+**DOB:** June 11, 1959
+**Age:** 65
+**Sex:** Male
+**Occupation:** Diagnostic Medicine Specialist (MD, Nephrology & Infectious Disease)
+**Date of Report:** September 16, 2025
+
+---
+
+## Chief Complaint
+Chronic leg pain and mobility impairment due to an infarction in the right quadriceps muscle following a misdiagnosed aneurysm.
+
+---
+
+## History of Present Illness (HPI)
+Dr. House experienced an infarction in his right thigh muscle approximately 20 years ago. Initial symptoms included severe pain and muscle necrosis, leading to partial removal of quadriceps muscle tissue. Since then, he has had chronic pain requiring long-term pain management. The condition has contributed to a dependence on a cane for mobility.
+
+---
+
+## Past Medical History
+- **Thigh Muscle Infarction** (right leg) – residual chronic pain.
+- **Chronic Pain Syndrome** – treated with long-term opioids.
+- **Vicodin Dependence** – longstanding usage with episodes of misuse.
+- **History of Depression** – secondary to chronic pain and functional limitation.
+- **Insomnia** – linked to pain and psychiatric comorbidity.
+
+---
+
+## Surgical History
+- Partial resection of necrotic right thigh muscle (circa early 2000s).
+
+---
+
+## Medications
+- **Hydrocodone/Acetaminophen (Vicodin)** – long-term use for pain management.
+- Occasional use of anti-anxiety medication and sleep aids.
+
+---
+
+## Allergies
+- **No known drug allergies (NKDA).**
+
+---
+
+## Family History
+- Father: abusive, estranged; military background.
+- Mother: emotionally distant.
+- No significant hereditary diseases reported.
+
+---
+
+## Social History
+- **Tobacco:** Denies use.
+- **Alcohol:** Social, occasional heavy episodes.
+- **Drugs:** History of opioid misuse.
+- **Occupation:** Renowned diagnostic physician at Princeton-Plainsboro Teaching Hospital (fictional).
+- **Marital Status:** Single; history of failed relationships.
+- **Support System:** Colleagues, notably Dr. James Wilson (oncology).
+
+---
+
+## Review of Systems
+- **Musculoskeletal:** Chronic right thigh pain, muscle weakness, gait impairment.
+- **Neurological:** Normal cognitive function; occasional irritability.
+- **Psychiatric:** Depression, obsessive tendencies, antisocial traits.
+- **Other systems:** Non-contributory.
+
+---
+
+## Physical Exam
+- **General:** Disheveled, often irritable but alert and oriented ×3.
+- **Gait:** Limp, uses cane.
+- **Extremities:** Atrophy and scarring on right thigh.
+- **Neuro:** Strength diminished in right lower extremity.
+- **Mood/Affect:** Blunt, sarcastic, defensive.
+
+---
+
+## Assessment
+1. Chronic right thigh pain secondary to infarction and surgical resection.
+2. Opioid dependence (Vicodin).
+3. Depression with maladaptive coping strategies.
+4. Functional limitations in mobility.
+
+---
+
+## Plan
+- Continue pain management with strict monitoring.
+- Consider referral to pain rehabilitation program.
+- Explore alternative analgesics to minimize opioid dependence.
+- Recommend psychiatric support for depression and substance use.
+- Encourage use of mobility aids and physical therapy.
+
+---
+
+**Physician’s Note:**
+Patient is resistant to psychiatric interventions and exhibits non-compliance with lifestyle recommendations. Insight into addiction is limited. Despite chronic conditions, he demonstrates exceptional diagnostic capabilities.
+
diff --git a/python/rag/healthcare-support-portal/sample-documents/policy-mindy-lahiri.md b/python/rag/healthcare-support-portal/sample-documents/policy-mindy-lahiri.md
new file mode 100644
index 00000000..f2e4ca16
--- /dev/null
+++ b/python/rag/healthcare-support-portal/sample-documents/policy-mindy-lahiri.md
@@ -0,0 +1,83 @@
+# Mindy Lahiri, M.D. – Medical Policy Handbook
+**Specialty:** Obstetrics & Gynecology
+**Location:** Shulman & Associates, NYC
+**Version:** 1.0
+**Date:** September 16, 2025
+
+---
+
+## Introduction
+Hi! I’m Dr. Mindy Lahiri, OB/GYN, lover of rom-coms, fashion, and being slightly extra.
+This medical policy handbook outlines how I practice medicine, keep patients safe, and stay fabulous at the same time.
+
+---
+
+## Philosophy of Care
+1. **Patient-Centered, Always:** Patients deserve respect, honesty, and a little charm.
+2. **Evidence-Based Medicine:** Science comes first (even if rom-com logic *sometimes* sneaks in).
+3. **Holistic Health:** Emotional wellbeing is just as important as physical health.
+4. **Boundaries:** No, I will not be your therapist. Unless you want to talk about your crush, then maybe.
+
+---
+
+## Clinical Guidelines
+
+### 1. Preventive Care
+- Annual well-woman exams.
+- Pap smears per ACOG guidelines.
+- HPV vaccination strongly encouraged.
+- Discussion of diet, exercise, and self-care routines (including *The Bachelor* if relevant).
+
+### 2. Prenatal & Obstetric Care
+- **Initial visit:** Comprehensive history, labs, ultrasound confirmation.
+- **Follow-up:** Regular monitoring every 4 weeks until 28 weeks, every 2 weeks until 36, then weekly until delivery.
+- **Labor & Delivery:** Respect for birth plans, but safety > Pinterest aesthetics.
+- **Postpartum:** Mental health screening mandatory.
+
+### 3. Gynecological Care
+- Contraceptive counseling: full spectrum, judgment-free.
+- Menstrual concerns: individualized treatment (from birth control to surgery).
+- Fertility consultations offered; referrals as needed.
+
+---
+
+## Office Policies
+
+### Appointments
+- Show up on time. I will try my best to also show up on time… emphasis on *try*.
+- Cancellations require 24-hour notice. Emergencies and celebrity sightings excused.
+
+### Communication
+- Secure messaging through clinic portal encouraged.
+- Texting me about medical results: **not allowed.**
+- Texting me about where to get the best cronuts: allowed.
+
+### Billing & Insurance
+- Standard medical billing applies.
+- Payment plans available.
+- I accept Venmo for coffee bribes, but not for surgery.
+
+---
+
+## Code of Conduct
+- Respect staff and other patients.
+- No “WebMD diagnosing” in the exam room — that’s *my* job.
+- Compliments on my outfit are always welcome but not required.
+
+---
+
+## Emergency Protocols
+- For emergencies: Call 911 or proceed to the nearest ER.
+- After-hours calls are triaged through the on-call service.
+- Do not DM me on Instagram for a medical emergency.
+
+---
+
+## Closing Statement
+This policy is designed to provide clarity, care, and maybe a little humor. At the end of the day, my patients are my priority — and if we can laugh while staying healthy, that’s the dream.
+
+**Signed,**
+*Mindy Lahiri, M.D.*
+Obstetrics & Gynecology
+Shulman & Associates
+
diff --git a/python/rag/healthcare-support-portal/sample-documents/protocol-grey-sloan-memorial.md b/python/rag/healthcare-support-portal/sample-documents/protocol-grey-sloan-memorial.md
new file mode 100644
index 00000000..06c73c07
--- /dev/null
+++ b/python/rag/healthcare-support-portal/sample-documents/protocol-grey-sloan-memorial.md
@@ -0,0 +1,66 @@
+# Grey Sloan Memorial Hospital
+**Protocol for Handling Overdramatized Events and One-in-a-Million Scenarios**
+**Version:** Drama Level 3000
+
+---
+
+## Purpose
+To provide clinicians, interns, and star-crossed lovers working at Grey Sloan Memorial with a standardized approach to handling medical events that statistically should *never* happen twice but, somehow, occur every other Thursday.
+
+---
+
+## Scope
+This protocol applies to:
+- Attending surgeons, especially those in complicated love triangles.
+- Surgical interns with suspiciously perfect hair.
+- Patients who arrive in the ER with only minutes to live but a full backstory montage ready.
+
+---
+
+## Definitions
+- **ODME (Overly Dramatized Medical Event):** A scenario where clinical probability is ignored in favor of maximum emotional payoff.
+- **1IAM (One-in-a-Million):** Medical occurrences that, by actuarial tables, should happen once per century but are standard issue at Grey Sloan.
+- **LVAD Wire Incident:** The gold standard of ethically dubious, emotionally charged, plot-driven medical choices.
+
+---
+
+## Standard Protocol
+
+### 1. Initial Response
+- All staff must immediately drop whatever ordinary case they are working on (appendectomies can wait).
+- Cue a tense music swell.
+- Ensure at least one attending is both personally invested and professionally conflicted.
+
+### 2. Triage of Overdramatized Events
+- **Meteor strikes:** Assess emotional fallout before physical injuries.
+- **Plane crashes:** Assign each attending one existential crisis per patient.
+- **Pregnant woman with simultaneous amnesia and rare tumor:** Page neuro, OB, and whichever resident is due for character growth.
+
+### 3. The LVAD Wire Rule
+- Cutting an LVAD wire is **never** recommended… unless it advances the love plot.
+- If performed:
+ - Ensure it’s done under dim lighting with maximum tears.
+ - Documentation should read: “Performed for narrative necessity.”
+
+### 4. One-in-a-Million Event Handling
+- Treat as if it’s the fiftieth time this month (because it probably is).
+- Assign at least one intern to freeze mid-surgery due to trauma flashbacks.
+- Remind everyone: Grey Sloan has no risk management department; only brooding in stairwells.
+
+### 5. Post-Event Debrief
+- Mandatory rooftop scene.
+- Minimum of three speeches containing the words *miracle*, *fate*, or *second chances*.
+- At least one character must dramatically resign but return two episodes later.
+
+---
+
+## Exception Handling
+- If multiple ODME events occur simultaneously (e.g., ferry crash + bomb in chest cavity), prioritize whichever involves more attractive guest stars.
+
+---
+
+## Closing Statement
+This protocol is designed not to save lives but to maximize Nielsen ratings and generate enough tears for at least one Emmy reel.
+
+**Approved by:**
+The Grey Sloan Memorial Committee on Plot Devices
diff --git a/python/rag/healthcare-support-portal/scripts/get_auth_token.sh b/python/rag/healthcare-support-portal/scripts/get_auth_token.sh
new file mode 100755
index 00000000..27616ce9
--- /dev/null
+++ b/python/rag/healthcare-support-portal/scripts/get_auth_token.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+# Get authentication token for dr_smith user
+# This script demonstrates the correct username format for API calls
+
+TOKEN=$(curl -s -X POST "http://localhost:8001/api/v1/auth/login" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "username=dr_smith&password=secure_password" | \
+ python3 -c "import sys, json; print(json.load(sys.stdin)['access_token'])")
+
+echo "Got auth token: ${TOKEN:0:20}..."
+
+# Export the token for use in other scripts
+export AUTH_TOKEN="$TOKEN"
+echo "Token exported as AUTH_TOKEN environment variable"
+echo "You can now use it in other curl commands like:"
+echo "curl -H \"Authorization: Bearer \$AUTH_TOKEN\" http://localhost:8002/api/v1/patients/"
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/scripts/migrate.sh b/python/rag/healthcare-support-portal/scripts/migrate.sh
new file mode 100755
index 00000000..a6fb375a
--- /dev/null
+++ b/python/rag/healthcare-support-portal/scripts/migrate.sh
@@ -0,0 +1,108 @@
+#!/bin/bash
+# scripts/migrate.sh - Run database migrations with retry logic and health checks
+
+set -e # Exit on error
+
+echo "🔄 Healthcare Support Portal - Database Migration"
+echo "================================================="
+
+# Get database URL from environment or use default
+DATABASE_URL="${DATABASE_URL:-postgresql+psycopg2://postgres:postgres@localhost:5432/healthcare}"
+
+# Extract connection details for psql
+DB_HOST=$(echo $DATABASE_URL | sed -n 's/.*@\([^:]*\).*/\1/p')
+DB_PORT=$(echo $DATABASE_URL | sed -n 's/.*:\([0-9]*\)\/.*/\1/p')
+DB_NAME=$(echo $DATABASE_URL | sed -n 's/.*\/\([^?]*\).*/\1/p')
+DB_USER=$(echo $DATABASE_URL | sed -n 's/.*\/\/\([^:]*\).*/\1/p')
+DB_PASS=$(echo $DATABASE_URL | sed -n 's/.*:\/\/[^:]*:\([^@]*\).*/\1/p')
+
+echo "📊 Database Configuration:"
+echo " Host: $DB_HOST"
+echo " Port: $DB_PORT"
+echo " Database: $DB_NAME"
+echo ""
+
+# Wait for database to be ready
+echo "⏳ Waiting for database to be ready..."
+MAX_WAIT=30
+for i in $(seq 1 $MAX_WAIT); do
+ if PGPASSWORD=$DB_PASS psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "SELECT 1" > /dev/null 2>&1; then
+ echo "✅ Database is ready!"
+ break
+ fi
+
+ if [ $i -eq $MAX_WAIT ]; then
+ echo "❌ Database is not available after ${MAX_WAIT} seconds"
+ exit 1
+ fi
+
+ echo " Waiting for database... (attempt $i/$MAX_WAIT)"
+ sleep 1
+done
+
+# Change to common package directory where alembic.ini is located
+cd /app/packages/common || cd packages/common
+
+echo ""
+echo "🔍 Checking current migration status..."
+# Use uv from PATH (works in both Docker and local environments)
+UV_CMD="uv"
+if [ -f "/opt/homebrew/bin/uv" ]; then
+ UV_CMD="/opt/homebrew/bin/uv"
+fi
+
+if $UV_CMD run alembic current 2>/dev/null; then
+ echo " Current migration retrieved successfully"
+else
+ echo " No migrations found (fresh database)"
+fi
+
+# Run migrations with retry logic
+echo ""
+echo "🚀 Running database migrations..."
+MAX_RETRIES=3
+
+for i in $(seq 1 $MAX_RETRIES); do
+ echo " Attempt $i of $MAX_RETRIES..."
+
+ if $UV_CMD run alembic upgrade head; then
+ echo ""
+ echo "✅ Migrations completed successfully!"
+
+ # Show current migration after success
+ echo ""
+ echo "📋 Current migration status:"
+ $UV_CMD run alembic current
+
+ # After successful migration, seed demo users if needed
+ echo ""
+ echo "🌱 Seeding demo users if not present..."
+ PGPASSWORD=$DB_PASS psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "
+ INSERT INTO users (id, username, email, hashed_password, role, department, is_active, created_at) VALUES
+ (1, 'admin_wilson', 'jennifer.wilson@hospital.com', '\$2b\$12\$gdtwLxe4YU648JwtZPX8/uAv9n5qpKZ8VFXJ1iyjqVU/HExd20IXC', 'admin', 'administration', true, NOW()),
+ (2, 'dr_smith', 'sarah.smith@hospital.com', '\$2b\$12\$xDK9vMsg6XD0wrbp9mTONeqZgViFvQGbUT9HMb2l1aU.nX5ssLnkS', 'doctor', 'cardiology', true, NOW()),
+ (3, 'nurse_johnson', 'michael.johnson@hospital.com', '\$2b\$12\$tVxonl15Y9xoKXKeBLQcX.mZO7ovvOtfRiI.vqPCqjRyuOVCmazfC', 'nurse', 'emergency', true, NOW())
+ ON CONFLICT (username) DO NOTHING;
+ " > /dev/null
+
+ # Check how many users were seeded
+ USER_COUNT=$(PGPASSWORD=$DB_PASS psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -t -c "SELECT COUNT(*) FROM users;" | tr -d ' ')
+ echo " ✅ Demo users available: $USER_COUNT (admin_wilson, dr_smith, nurse_johnson)"
+ echo " 🔑 All demo passwords: secure_password"
+
+ exit 0
+ fi
+
+ if [ $i -eq $MAX_RETRIES ]; then
+ echo ""
+ echo "❌ Migration failed after $MAX_RETRIES attempts"
+ echo " Please check the error messages above and ensure:"
+ echo " 1. The database is accessible"
+ echo " 2. The database user has proper permissions"
+ echo " 3. The migration files are valid"
+ exit 1
+ fi
+
+ echo "⚠️ Migration attempt $i failed, retrying in 2 seconds..."
+ sleep 2
+done
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/scripts/test_ui_fixes.md b/python/rag/healthcare-support-portal/scripts/test_ui_fixes.md
new file mode 100644
index 00000000..1fa05bbf
--- /dev/null
+++ b/python/rag/healthcare-support-portal/scripts/test_ui_fixes.md
@@ -0,0 +1,75 @@
+# UI Fixes Test Checklist
+
+## Issues Fixed
+
+### 1. Username Display Fix ✅
+**Issue**: "Good afternoon, Dr. dr_smith" displayed in homepage
+**Fix**: Modified `_index.tsx` to use `formatDisplayName()` function
+**Test**:
+- [ ] Login as dr_smith
+- [ ] Check dashboard displays "Good afternoon, Dr. Smith" instead of "Dr. dr_smith"
+- [ ] Check other users show correct format (e.g., "Administrator Wilson")
+
+### 2. Document Upload Button Fix ✅
+**Issue**: Upload button not working - UI error on click
+**Fix**:
+- Fixed API method parameter order in `api.server.ts`
+- Added better error logging in `DocumentUpload.tsx`
+- Improved error handling with console logs
+**Test**:
+- [ ] Navigate to Documents section
+- [ ] Click on upload area or "Choose Files" button
+- [ ] Verify file selection dialog opens
+- [ ] Select a file and check upload process works
+- [ ] Check browser console for any errors
+
+### 3. Documents Section Access Fix ✅
+**Issue**: Can't access documents section - UI error
+**Fix**:
+- Verified routing in `routes.ts` is correct
+- Checked navigation permissions in `Navigation.tsx`
+- Documents section should be accessible to all authenticated users
+**Test**:
+- [ ] Login as dr_smith (doctor role)
+- [ ] Click "Documents" in navigation sidebar
+- [ ] Verify page loads without errors
+- [ ] Check page shows documents list and upload section
+
+## Test Steps
+
+1. **Start the application**:
+ ```bash
+ cd /Users/erinmikail/GitHub-Local/sdk-examples/python/rag/healthcare-support-portal
+ ./run_all.sh
+ ```
+
+2. **Wait for services to start** (check logs for "ready" messages)
+
+3. **Access the frontend**: http://localhost:3000
+
+4. **Login with test credentials**:
+ - Username: `dr_smith`
+ - Password: `secure_password`
+
+5. **Test each fix**:
+ - [ ] Check homepage greeting
+ - [ ] Navigate to Documents
+ - [ ] Test document upload
+
+## Expected Results
+
+- ✅ Homepage shows "Good afternoon, Dr. Smith" (not "Dr. dr_smith")
+- ✅ Documents section loads without errors
+- ✅ Upload button/area responds to clicks and opens file dialog
+- ✅ No JavaScript console errors
+
+## If Issues Persist
+
+1. Check browser console (F12) for JavaScript errors
+2. Check network tab for failed API calls
+3. Check backend service logs for authentication/upload errors
+4. Verify all services are running:
+ - Auth service: http://localhost:8001/health
+ - Patient service: http://localhost:8002/health
+ - RAG service: http://localhost:8003/health
+ - Frontend: http://localhost:3000
\ No newline at end of file
diff --git a/python/rag/healthcare-support-portal/setup.sh b/python/rag/healthcare-support-portal/setup.sh
new file mode 100755
index 00000000..d292a44f
--- /dev/null
+++ b/python/rag/healthcare-support-portal/setup.sh
@@ -0,0 +1,189 @@
+#!/bin/bash
+# setup.sh - Cross-platform initial setup for Healthcare Support Portal
+# Compatible with macOS, Linux, and Windows (via Git Bash or WSL)
+
+# Exit on any error
+set -e
+
+# Detect operating system
+OS="Unknown"
+case "$(uname -s)" in
+ Darwin*) OS="macOS";;
+ Linux*) OS="Linux";;
+ CYGWIN*) OS="Windows";;
+ MINGW*) OS="Windows";;
+ MSYS*) OS="Windows";;
+esac
+
+echo "🏥 Healthcare Support Portal - Initial Setup ($OS)"
+echo "================================================="
+
+# Function to check if a command exists
+command_exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+# Function to compare version numbers
+version_ge() {
+ test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
+}
+
+# Create necessary directories
+echo "📁 Creating directories..."
+mkdir -p logs
+mkdir -p data/postgres
+
+# Check prerequisites
+echo "🔍 Checking prerequisites..."
+
+# Check Python
+if ! command_exists python3; then
+ echo "❌ Python 3 is not installed. Please install Python 3.11+ and try again."
+ exit 1
+fi
+
+PYTHON_VERSION=$(python3 --version | cut -d' ' -f2)
+if ! version_ge "$PYTHON_VERSION" "3.11.0"; then
+ echo "❌ Python $PYTHON_VERSION found, but 3.11+ is required."
+ exit 1
+fi
+echo "✅ Python $PYTHON_VERSION is compatible"
+
+# Check uv
+UV_CMD="uv"
+if [ -f "/opt/homebrew/bin/uv" ]; then
+ UV_CMD="/opt/homebrew/bin/uv"
+fi
+
+if ! command_exists "$UV_CMD"; then
+ echo "❌ uv package manager not found. Installing..."
+ if command_exists curl; then
+ curl -LsSf https://astral.sh/uv/install.sh | sh
+ export PATH="$HOME/.local/bin:$PATH"
+ UV_CMD="uv"
+ else
+ echo "❌ curl not found. Please install uv manually: https://docs.astral.sh/uv/getting-started/installation/"
+ exit 1
+ fi
+fi
+echo "✅ uv package manager found"
+
+# Set up Python environment
+echo "🐍 Setting up Python environment..."
+if ! $UV_CMD sync; then
+ echo "❌ Failed to sync Python dependencies with uv"
+ exit 1
+fi
+echo "✅ Python dependencies synced"
+
+# Copy example .env files
+echo "📋 Setting up environment files..."
+for service in auth patient rag; do
+ if [ ! -f "packages/$service/.env" ]; then
+ if [ -f "packages/$service/.env.example" ]; then
+ cp "packages/$service/.env.example" "packages/$service/.env"
+ echo "✅ Created packages/$service/.env from example"
+ fi
+ else
+ echo "⚠️ packages/$service/.env already exists, skipping"
+ fi
+done
+
+# Generate a secure SECRET_KEY
+echo "🔑 Generating secure SECRET_KEY..."
+SECRET_KEY=$($UV_CMD run python -c "import secrets; print(secrets.token_urlsafe(32))")
+
+# Update .env files with the generated SECRET_KEY
+echo "🔄 Updating .env files with generated SECRET_KEY..."
+for service in auth patient rag; do
+ if [ -f "packages/$service/.env" ]; then
+ sed -i.bak "s/SECRET_KEY=change-me-in-production/SECRET_KEY=$SECRET_KEY/" "packages/$service/.env"
+ rm "packages/$service/.env.bak" 2>/dev/null || true
+ echo "✅ Updated SECRET_KEY in packages/$service/.env"
+ fi
+done
+
+# Make scripts executable (Unix/Linux/macOS only)
+if [ "$OS" != "Windows" ]; then
+ echo "🔧 Making scripts executable..."
+ chmod +x run_all.sh
+ chmod +x stop_all.sh
+ chmod +x packages/auth/run.sh
+ chmod +x packages/patient/run.sh
+ chmod +x packages/rag/run.sh
+ chmod +x frontend/run.sh
+else
+ echo "🔧 Scripts ready (Windows detected - no chmod needed)..."
+fi
+
+# Install frontend dependencies
+echo "📦 Installing frontend dependencies..."
+
+# Check Node.js
+if ! command_exists node; then
+ echo "❌ Node.js not found. Please install Node.js 20.19.0+ from https://nodejs.org/"
+ echo " After installation, run: cd frontend && npm install"
+ echo "⚠️ Continuing with backend setup only..."
+else
+ NODE_VERSION=$(node --version | sed 's/v//')
+ if version_ge "$NODE_VERSION" "20.19.0"; then
+ echo "✅ Node.js $NODE_VERSION is compatible"
+
+ # Check npm
+ if ! command_exists npm; then
+ echo "❌ npm not found. Please reinstall Node.js or install npm separately."
+ exit 1
+ fi
+
+ # Install frontend dependencies
+ if [ -f "frontend/package.json" ]; then
+ echo "📦 Installing frontend packages with npm..."
+ cd frontend
+ if npm install; then
+ echo "✅ Frontend dependencies installed successfully"
+ else
+ echo "❌ Failed to install frontend dependencies"
+ cd ..
+ exit 1
+ fi
+ cd ..
+ else
+ echo "❌ frontend/package.json not found"
+ exit 1
+ fi
+ else
+ echo "❌ Node.js $NODE_VERSION found, but 20.19.0+ is required."
+ echo " Please upgrade Node.js and run: cd frontend && npm install"
+ echo "⚠️ Continuing with backend setup only..."
+ fi
+fi
+
+echo ""
+echo "✅ Setup complete!"
+echo ""
+echo "📝 Next steps:"
+echo "1. 🔑 Add your OpenAI API key to packages/rag/.env (REQUIRED)"
+echo " - Get your key at: https://platform.openai.com/api-keys"
+echo " - Replace '***************************' with your actual key"
+
+if [ "$OS" = "Windows" ]; then
+ echo "2. 🚀 Start all services:"
+ echo " - Using Docker: docker-compose up -d"
+ echo " - Or manually: Start each service in separate terminals"
+ echo " - Windows users: Use 'bash run_all.sh' or run services individually"
+else
+ echo "2. 🚀 Start all services: ./run_all.sh"
+fi
+
+echo "3. 🌱 Seed demo data: $UV_CMD run python -m common.seed_data"
+echo "4. 🌐 Open http://localhost:3000 in your browser"
+echo ""
+echo "🔐 Demo Login Credentials (after seeding):"
+echo " Doctor: dr_smith / secure_password"
+echo " Nurse: nurse_johnson / secure_password"
+echo " Admin: admin_wilson / secure_password"
+echo ""
+echo "📚 API Documentation will be available at:"
+echo " 🔐 Auth Service: http://localhost:8001/docs"
+echo " 🏥 Patient Service: http://localhost:8002/docs"
+echo " 🤖 RAG Service: http://localhost:8003/docs"
diff --git a/python/rag/healthcare-support-portal/stop_all.sh b/python/rag/healthcare-support-portal/stop_all.sh
new file mode 100755
index 00000000..9aef3070
--- /dev/null
+++ b/python/rag/healthcare-support-portal/stop_all.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+# stop_all.sh - Stop all Healthcare Support Portal services
+
+# Don't exit on error for this script (we want to try stopping all services)
+# set -e would cause the script to exit if one service isn't running
+
+echo "🛑 Healthcare Support Portal - Stopping All Services"
+echo "=================================================="
+
+# Function to stop a service by PID file
+stop_service() {
+ local service_name=$1
+ local pid_file="logs/${service_name}.pid"
+
+ if [ -f "$pid_file" ]; then
+ local pid=$(cat "$pid_file")
+ if kill -0 "$pid" 2>/dev/null; then
+ echo "🛑 Stopping $service_name (PID: $pid)..."
+ kill "$pid"
+ rm "$pid_file"
+ else
+ echo "⚠️ $service_name was not running (stale PID file)"
+ rm "$pid_file"
+ fi
+ else
+ echo "⚠️ No PID file found for $service_name"
+ fi
+}
+
+# Stop all services
+stop_service "auth"
+stop_service "patient"
+stop_service "rag"
+stop_service "frontend"
+
+# Also try to kill any remaining uvicorn processes for our services
+echo "🧹 Cleaning up any remaining processes..."
+if command -v pkill >/dev/null 2>&1; then
+ pkill -f "src.auth_service.main:app" 2>/dev/null || true
+ pkill -f "src.patient_service.main:app" 2>/dev/null || true
+ pkill -f "src.rag_service.main:app" 2>/dev/null || true
+ # Also kill any npm/node processes for frontend
+ pkill -f "react-router dev" 2>/dev/null || true
+else
+ echo "⚠️ pkill not available. Manual cleanup may be needed."
+fi
+
+echo ""
+echo "✅ All services stopped!"
+echo "🗄️ Database is still running. Use 'docker-compose down' to stop it."
+echo " To stop everything including database: docker-compose down"
diff --git a/python/rag/healthcare-support-portal/sync_facts.sh b/python/rag/healthcare-support-portal/sync_facts.sh
new file mode 100755
index 00000000..7bc75841
--- /dev/null
+++ b/python/rag/healthcare-support-portal/sync_facts.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+# Healthcare Support Portal - OSO Facts Synchronization
+# Convenience script to sync authorization facts with OSO Cloud
+
+# Exit on any error
+set -e
+
+# Function to check if a command exists
+command_exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+echo "🔐 Healthcare Support Portal - OSO Fact Synchronization"
+echo "======================================================"
+
+# Check for uv command
+UV_CMD="uv"
+if [ -f "/opt/homebrew/bin/uv" ]; then
+ UV_CMD="/opt/homebrew/bin/uv"
+fi
+
+if ! command_exists "$UV_CMD"; then
+ echo "❌ uv package manager not found. Please install uv and try again."
+ echo " Installation: curl -LsSf https://astral.sh/uv/install.sh | sh"
+ exit 1
+fi
+
+# Check if we're in the right directory
+if [ ! -f "packages/common/src/common/sync_oso_facts.py" ] && [ ! -f "sync_oso_facts.py" ]; then
+ echo "❌ Cannot find sync_oso_facts.py script."
+ echo " Please run this from the project root directory."
+ exit 1
+fi
+
+echo "🚀 Running fact synchronization..."
+
+# Run the fact synchronization script
+if ! $UV_CMD run python -m packages.common.src.common.sync_oso_facts; then
+ echo "❌ Failed to synchronize OSO facts"
+ exit 1
+fi
+
+echo "✅ OSO facts synchronized successfully!"
diff --git a/python/rag/healthcare-support-portal/test_document.txt b/python/rag/healthcare-support-portal/test_document.txt
new file mode 100644
index 00000000..d992e45e
--- /dev/null
+++ b/python/rag/healthcare-support-portal/test_document.txt
@@ -0,0 +1,41 @@
+Healthcare Protocol: Patient Admission Guidelines
+
+OVERVIEW
+This document outlines the standard procedures for patient admission to the cardiology department.
+
+ADMISSION CRITERIA
+1. Patients presenting with chest pain lasting more than 30 minutes
+2. Abnormal ECG findings indicating cardiac abnormalities
+3. Elevated cardiac biomarkers (troponin levels above normal)
+4. Physician referral for specialized cardiac evaluation
+
+PROCEDURES
+1. Initial Assessment
+ - Vital signs monitoring every 15 minutes for first hour
+ - 12-lead ECG within 10 minutes of arrival
+ - Blood draw for cardiac enzymes and lipid panel
+
+2. Documentation Requirements
+ - Complete medical history
+ - Current medications list
+ - Emergency contact information
+ - Insurance verification
+
+3. Treatment Protocol
+ - Oxygen therapy if saturation below 94%
+ - Continuous cardiac monitoring
+ - Pain assessment using 1-10 scale
+ - Antiplatelet therapy as indicated
+
+SPECIAL CONSIDERATIONS
+- Patients with diabetes require glucose monitoring every 4 hours
+- Elderly patients (>75 years) need fall risk assessment
+- Consider cardiothoracic surgery consultation for severe cases
+
+DISCHARGE CRITERIA
+- Stable vital signs for 24 hours
+- Normal or stable cardiac enzymes
+- Patient education completed
+- Follow-up appointment scheduled
+
+This protocol should be reviewed quarterly and updated as needed based on current medical guidelines and research findings.
diff --git a/python/rag/healthcare-support-portal/validate_environment.sh b/python/rag/healthcare-support-portal/validate_environment.sh
new file mode 100755
index 00000000..ecfc3dde
--- /dev/null
+++ b/python/rag/healthcare-support-portal/validate_environment.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+#
+# Healthcare Support Portal - Environment Validation Script
+# This script checks that all required dependencies and ports are available
+#
+
+echo "🔍 Validating environment..."
+echo "=============================="
+
+# Check required software
+echo "📋 Checking required software:"
+
+python3 --version >/dev/null 2>&1 && echo "✅ Python OK" || echo "❌ Install Python 3.11+"
+node --version >/dev/null 2>&1 && echo "✅ Node.js OK" || echo "❌ Install Node.js 20.19.0+"
+docker --version >/dev/null 2>&1 && echo "✅ Docker OK" || echo "❌ Install Docker"
+git --version >/dev/null 2>&1 && echo "✅ Git OK" || echo "❌ Install Git"
+command -v uv >/dev/null 2>&1 && echo "✅ UV package manager OK" || echo "❌ Install UV package manager"
+
+echo
+echo "🔌 Checking port availability:"
+
+# Check available ports
+for port in 3000 8001 8002 8003 5432 8080; do
+ if ! lsof -i :$port > /dev/null 2>&1; then
+ echo "✅ Port $port available"
+ else
+ echo "⚠️ Port $port in use - you may need to stop other services"
+ fi
+done
+
+echo
+echo "🎉 Environment validation complete!"
+echo "If any checks failed, see the troubleshooting section in README.md"