fazeel007 commited on
Commit
7c012de
·
1 Parent(s): bfbe43b

initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +18 -0
  2. .gitattributes copy +35 -0
  3. .gitignore +78 -0
  4. .replit +37 -0
  5. Dockerfile +45 -0
  6. README copy.md +344 -0
  7. app.py +27 -0
  8. client/index.html +13 -0
  9. client/src/App.tsx +35 -0
  10. client/src/components/ErrorBoundary.tsx +75 -0
  11. client/src/components/knowledge-base/ai-assistant.tsx +507 -0
  12. client/src/components/knowledge-base/citation-panel.tsx +133 -0
  13. client/src/components/knowledge-base/enhanced-search-interface.tsx +603 -0
  14. client/src/components/knowledge-base/knowledge-graph.tsx +811 -0
  15. client/src/components/knowledge-base/result-card.tsx +509 -0
  16. client/src/components/knowledge-base/search-interface.tsx +181 -0
  17. client/src/components/knowledge-base/search-results.tsx +145 -0
  18. client/src/components/knowledge-base/system-flow-diagram.tsx +491 -0
  19. client/src/components/theme-provider.tsx +64 -0
  20. client/src/components/theme-toggle.tsx +20 -0
  21. client/src/components/ui/accordion.tsx +56 -0
  22. client/src/components/ui/alert-dialog.tsx +139 -0
  23. client/src/components/ui/alert.tsx +59 -0
  24. client/src/components/ui/aspect-ratio.tsx +5 -0
  25. client/src/components/ui/avatar.tsx +50 -0
  26. client/src/components/ui/badge.tsx +36 -0
  27. client/src/components/ui/breadcrumb.tsx +115 -0
  28. client/src/components/ui/button.tsx +56 -0
  29. client/src/components/ui/calendar.tsx +68 -0
  30. client/src/components/ui/card.tsx +79 -0
  31. client/src/components/ui/carousel.tsx +260 -0
  32. client/src/components/ui/chart.tsx +365 -0
  33. client/src/components/ui/checkbox.tsx +28 -0
  34. client/src/components/ui/collapsible.tsx +11 -0
  35. client/src/components/ui/command.tsx +151 -0
  36. client/src/components/ui/context-menu.tsx +198 -0
  37. client/src/components/ui/dialog.tsx +122 -0
  38. client/src/components/ui/drawer.tsx +118 -0
  39. client/src/components/ui/dropdown-menu.tsx +198 -0
  40. client/src/components/ui/form.tsx +178 -0
  41. client/src/components/ui/hover-card.tsx +29 -0
  42. client/src/components/ui/input-otp.tsx +69 -0
  43. client/src/components/ui/input.tsx +25 -0
  44. client/src/components/ui/label.tsx +24 -0
  45. client/src/components/ui/menubar.tsx +256 -0
  46. client/src/components/ui/navigation-menu.tsx +128 -0
  47. client/src/components/ui/pagination.tsx +117 -0
  48. client/src/components/ui/popover.tsx +29 -0
  49. client/src/components/ui/progress.tsx +28 -0
  50. client/src/components/ui/radio-group.tsx +42 -0
.env.example ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # KnowledgeBridge Environment Configuration
2
+ # Copy this file to .env and fill in your API keys
3
+
4
+ # Nebius AI Configuration (Required for AI features)
5
+ NEBIUS_API_KEY=your_nebius_api_key_here
6
+
7
+ # Modal Configuration (Optional - for distributed processing)
8
+ MODAL_TOKEN_ID=your_modal_token_id
9
+ MODAL_TOKEN_SECRET=your_modal_token_secret
10
+ MODAL_BASE_URL=your_modal_endpoint
11
+
12
+ # GitHub Configuration (Optional - for repository search)
13
+ GITHUB_TOKEN=your_github_personal_access_token
14
+
15
+ # Node Environment
16
+ NODE_ENV=production
17
+ PORT=7860
18
+ EOF < /dev/null
.gitattributes copy ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ # dist - commented out for Hugging Face Spaces deployment
3
+ .DS_Store
4
+ server/public
5
+ vite.config.ts.*
6
+ *.tar.gz
7
+ attached_assets
8
+ server.log
9
+
10
+ # Environment files
11
+ .env
12
+ .env.local
13
+ .env.production
14
+ .env.development
15
+
16
+ # Logs
17
+ *.log
18
+ npm-debug.log*
19
+ yarn-debug.log*
20
+ yarn-error.log*
21
+
22
+ # Runtime data
23
+ pids
24
+ *.pid
25
+ *.seed
26
+ *.pid.lock
27
+
28
+ # Python
29
+ __pycache__/
30
+ *.py[cod]
31
+ *$py.class
32
+ *.so
33
+ .Python
34
+ build/
35
+ develop-eggs/
36
+ dist/
37
+ downloads/
38
+ eggs/
39
+ .eggs/
40
+ lib/
41
+ lib64/
42
+ parts/
43
+ sdist/
44
+ var/
45
+ wheels/
46
+ *.egg-info/
47
+ .installed.cfg
48
+ *.egg
49
+ MANIFEST
50
+
51
+ # Virtual environments
52
+ venv/
53
+ env/
54
+ ENV/
55
+ env.bak/
56
+ venv.bak/
57
+
58
+ # IDE
59
+ .vscode/
60
+ .idea/
61
+ *.swp
62
+ *.swo
63
+
64
+ # OS
65
+ .DS_Store
66
+ Thumbs.db
67
+
68
+ # Test coverage
69
+ coverage/
70
+ .coverage
71
+ .nyc_output
72
+ htmlcov/
73
+
74
+ # Temporary files
75
+ *.tmp
76
+ *.temp
77
+ temp/
78
+ tmp/
.replit ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modules = ["nodejs-20", "web", "postgresql-16", "python-3.11"]
2
+ run = "npm run dev"
3
+ hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
4
+
5
+ [nix]
6
+ channel = "stable-24_05"
7
+ packages = ["ffmpeg-full"]
8
+
9
+ [deployment]
10
+ deploymentTarget = "autoscale"
11
+ build = ["npm", "run", "build"]
12
+ run = ["npm", "run", "start"]
13
+
14
+ [[ports]]
15
+ localPort = 5000
16
+ externalPort = 80
17
+
18
+ [workflows]
19
+ runButton = "Project"
20
+
21
+ [[workflows.workflow]]
22
+ name = "Project"
23
+ mode = "parallel"
24
+ author = "agent"
25
+
26
+ [[workflows.workflow.tasks]]
27
+ task = "workflow.run"
28
+ args = "Start application"
29
+
30
+ [[workflows.workflow]]
31
+ name = "Start application"
32
+ author = "agent"
33
+
34
+ [[workflows.workflow.tasks]]
35
+ task = "shell.exec"
36
+ args = "npm run dev"
37
+ waitForPort = 5000
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Node.js 18 Alpine for smaller image size
2
+ FROM node:18-alpine
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apk add --no-cache \
9
+ python3 \
10
+ make \
11
+ g++ \
12
+ curl \
13
+ && ln -sf python3 /usr/bin/python
14
+
15
+ # Copy package files
16
+ COPY package*.json ./
17
+
18
+ # Install dependencies
19
+ RUN npm ci --only=production
20
+
21
+ # Copy source code
22
+ COPY . .
23
+
24
+ # Build the application
25
+ RUN npm run build
26
+
27
+ # Create non-root user for security
28
+ RUN addgroup -g 1001 -S nodejs && \
29
+ adduser -S nextjs -u 1001
30
+
31
+ # Change ownership of the app directory
32
+ RUN chown -R nextjs:nodejs /app
33
+
34
+ # Switch to non-root user
35
+ USER nextjs
36
+
37
+ # Expose port
38
+ EXPOSE 7860
39
+
40
+ # Health check
41
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
42
+ CMD curl -f http://localhost:7860/api/health || exit 1
43
+
44
+ # Start the application
45
+ CMD ["npm", "start"]
README copy.md ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: KnowledgeBridge
3
+ emoji: 📚
4
+ colorFrom: yellow
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ short_description: 'A sophisticated AI-powered knowledge retrieval and analysis '
10
+ tags:
11
+ - agent-demo-track
12
+ ---
13
+
14
+ # KnowledgeBridge
15
+
16
+ 🚀 **An AI-Enhanced Knowledge Discovery Platform**
17
+
18
+ A sophisticated AI-powered knowledge retrieval and analysis system that combines semantic search, real-time web integration, and intelligent document processing for research and information discovery.
19
+
20
+ ![Security Status](https://img.shields.io/badge/Security-Hardened-green)
21
+ ![TypeScript](https://img.shields.io/badge/TypeScript-100%25-blue)
22
+ ![AI Models](https://img.shields.io/badge/AI-Nebius%20DeepSeek-purple)
23
+ ![License](https://img.shields.io/badge/License-MIT-yellow)
24
+
25
+ ## 🎯 Hackathon Submission
26
+
27
+ **🤖 Track 3: Agentic Demo Showcase**
28
+
29
+ **Submitted to**: [Hugging Face Agents-MCP-Hackathon](https://huggingface.co/Agents-MCP-Hackathon)
30
+
31
+ **Live Demo**: [Try KnowledgeBridge on Hugging Face Spaces](https://huggingface.co/spaces/YOUR_USERNAME/KnowledgeBridge)
32
+
33
+ ### **🚀 "Show us the most incredible things that your agents can do!"**
34
+
35
+ KnowledgeBridge demonstrates sophisticated AI agent orchestration through multi-modal knowledge discovery, intelligent query enhancement, and autonomous research synthesis.
36
+
37
+ ## 🤖 Agentic Capabilities Showcase
38
+
39
+ ### 🧠 **Multi-Agent Orchestration**
40
+ - **Coordinated Search Agents**: Simultaneous deployment across GitHub, Wikipedia, ArXiv, and web sources
41
+ - **Intelligent Load Balancing**: Agents dynamically distribute workload based on query type and source availability
42
+ - **Fallback Agent Strategy**: Backup agents activate when primary sources fail or timeout
43
+ - **Real-Time Coordination**: Agents communicate results and adapt search strategies collaboratively
44
+
45
+ ### 🔍 **Query Enhancement Agents**
46
+ - **Intent Recognition Agents**: AI agents analyze user intent and suggest optimal search strategies
47
+ - **Semantic Expansion Agents**: Agents enhance queries with related terms and concepts
48
+ - **Context-Aware Agents**: Agents consider previous searches and user preferences
49
+ - **Multi-Modal Query Agents**: Agents adapt search approach based on content type (code, academic, general)
50
+
51
+ ### 📊 **Analysis & Synthesis Agents**
52
+ - **Document Processing Agents**: Autonomous analysis with configurable reasoning (summary, classification, key points)
53
+ - **Research Synthesis Agents**: AI agents combine insights from multiple sources into coherent analysis
54
+ - **Quality Assessment Agents**: Agents evaluate source credibility and content relevance
55
+ - **Format Adaptation Agents**: Agents dynamically adjust output format (markdown/plain text) based on user needs
56
+
57
+ ### 🛡️ **Security & Validation Agents**
58
+ - **URL Validation Agents**: Intelligent agents verify link accessibility and content authenticity
59
+ - **Rate Limiting Agents**: Protective agents prevent API abuse (100 requests/15min, 10/min for sensitive endpoints)
60
+ - **Input Sanitization Agents**: Security agents validate and clean all user inputs
61
+ - **Error Recovery Agents**: Resilient agents handle failures gracefully and maintain system stability
62
+
63
+ ### 🌐 **Intelligent Integration Agents**
64
+ - **ArXiv Academic Agents**: Specialized agents for academic paper validation and retrieval
65
+ - **GitHub Repository Agents**: Code-focused agents with author filtering and relevance scoring
66
+ - **Wikipedia Knowledge Agents**: Authoritative content agents with intelligent caching strategies
67
+ - **Cross-Platform Synthesis Agents**: Agents that combine and rank results across all sources
68
+
69
+ ## 🏗️ Technical Architecture
70
+
71
+ ### **Frontend Stack**
72
+ - **React 18** with TypeScript for type-safe development
73
+ - **Wouter Router** for lightweight client-side routing
74
+ - **TanStack Query** for efficient data fetching and caching
75
+ - **Radix UI + Tailwind CSS** for accessible, modern components
76
+ - **Framer Motion** for smooth animations and transitions
77
+
78
+ ### **Backend Stack**
79
+ - **Node.js + Express** with comprehensive middleware
80
+ - **Nebius AI** integration with DeepSeek models
81
+ - **Modal** for distributed processing and scalability
82
+ - **Express Rate Limit** for API protection
83
+ - **Helmet.js** for security headers
84
+
85
+ ### **AI & Processing**
86
+ - **DeepSeek-R1-0528** for chat completions and document analysis
87
+ - **BAAI/bge-en-icl** for embedding generation
88
+ - **Modal Client** for distributed compute tasks
89
+ - **Smart Ingestion Service** for advanced document processing
90
+
91
+ ## 🚀 Quick Start
92
+
93
+ ### **Environment Configuration**
94
+
95
+ Create a `.env` file in the project root:
96
+
97
+ ```bash
98
+ # Nebius AI Configuration (Required)
99
+ NEBIUS_API_KEY=your_nebius_api_key_here
100
+
101
+ # Modal Configuration (Optional - for advanced processing)
102
+ MODAL_TOKEN_ID=your_modal_token_id
103
+ MODAL_TOKEN_SECRET=your_modal_token_secret
104
+ MODAL_BASE_URL=your_modal_endpoint
105
+
106
+ # GitHub Configuration (Optional - for repository search)
107
+ GITHUB_TOKEN=your_github_token_here
108
+
109
+ # Node Environment
110
+ NODE_ENV=development
111
+ ```
112
+
113
+ ### **Development Setup**
114
+
115
+ ```bash
116
+ # Install dependencies
117
+ npm install
118
+
119
+ # Start development server
120
+ npm run dev
121
+
122
+ # Build for production
123
+ npm run build
124
+
125
+ # Type checking
126
+ npm run check
127
+ ```
128
+
129
+ The application will be available at `http://localhost:5000`
130
+
131
+ ## 🎯 Usage Guide
132
+
133
+ ### **Search Interface**
134
+ 1. **Basic Search**: Enter queries in natural language
135
+ 2. **AI Enhancement**: Click the sparkle icon to improve your query
136
+ 3. **Advanced Search**: Use the AI tools panel for document analysis
137
+ 4. **Export Results**: Generate citations in multiple formats
138
+
139
+ ### **AI Tools**
140
+ - **Document Analysis**: Paste content for AI-powered analysis with configurable formatting
141
+ - **Embeddings**: Generate vector representations of text
142
+ - **Query Enhancement**: Get AI suggestions for better search queries
143
+
144
+ ### **Knowledge Graph**
145
+ - Interactive visualization of document relationships
146
+ - Filter by concepts, authors, and source types
147
+ - Explore connections between research papers and topics
148
+
149
+ ## 🔧 API Reference
150
+
151
+ ### **Search Endpoints**
152
+ ```typescript
153
+ POST /api/search
154
+ {
155
+ query: string;
156
+ searchType: "semantic" | "keyword" | "hybrid";
157
+ limit: number;
158
+ filters?: {
159
+ sourceTypes?: string[];
160
+ };
161
+ }
162
+ ```
163
+
164
+ ### **AI Analysis Endpoints**
165
+ ```typescript
166
+ POST /api/analyze-document
167
+ {
168
+ content: string;
169
+ analysisType: "summary" | "classification" | "key_points" | "quality_score";
170
+ useMarkdown?: boolean;
171
+ }
172
+
173
+ POST /api/enhance-query
174
+ {
175
+ query: string;
176
+ context?: string;
177
+ }
178
+
179
+ POST /api/embeddings
180
+ {
181
+ input: string;
182
+ model?: string;
183
+ }
184
+ ```
185
+
186
+ ### **Health Check**
187
+ ```typescript
188
+ GET /api/health
189
+ // Returns comprehensive health status of all services
190
+ ```
191
+
192
+ ## 🚀 Performance & Reliability
193
+
194
+ ### **Response Times**
195
+ - Local search: <100ms for semantic queries
196
+ - Document analysis: ~3-5 seconds depending on content length
197
+ - URL validation: <2 seconds per URL with concurrent processing
198
+ - Embedding generation: ~500ms-1s per request
199
+
200
+ ### **Scalability Features**
201
+ - Rate limiting prevents API abuse
202
+ - Concurrent URL validation with configurable limits
203
+ - Efficient caching for repeated queries
204
+ - Graceful degradation when external services are unavailable
205
+
206
+ ### **Error Handling**
207
+ - React Error Boundaries prevent UI crashes
208
+ - Comprehensive API error responses
209
+ - Automatic retry logic for network requests
210
+ - User-friendly error messages
211
+
212
+ ## 🔒 Security Features
213
+
214
+ ### **Input Protection**
215
+ - Request body size limits (10MB)
216
+ - Comprehensive input sanitization
217
+ - SQL injection prevention
218
+ - XSS protection with CSP headers
219
+
220
+ ### **API Security**
221
+ - Rate limiting on all endpoints
222
+ - Secure environment variable handling
223
+ - No hardcoded credentials
224
+ - Proper error logging without information disclosure
225
+
226
+ ### **Infrastructure Security**
227
+ - Helmet.js security headers
228
+ - CORS configuration
229
+ - Secure cookie handling
230
+ - Production-ready error handling
231
+
232
+ ## 🛠️ Development
233
+
234
+ ### **Code Quality**
235
+ - 100% TypeScript coverage
236
+ - ESLint + Prettier configuration
237
+ - Comprehensive error handling
238
+ - Type-safe API contracts with Zod validation
239
+
240
+ ### **Testing**
241
+ ```bash
242
+ # Type checking
243
+ npm run check
244
+
245
+ # Development server
246
+ npm run dev
247
+
248
+ # Production build
249
+ npm run build
250
+ ```
251
+
252
+ ## 🎉 Recent Updates
253
+
254
+ - ✅ **Security Hardening**: Removed all hardcoded credentials, added comprehensive security middleware
255
+ - ✅ **TypeScript Migration**: Achieved 100% type safety across the entire codebase
256
+ - ✅ **URL Validation**: Intelligent filtering of broken and invalid links
257
+ - ✅ **Error Handling**: React Error Boundaries and improved server error handling
258
+ - ✅ **AI Enhancement**: Nebius AI integration with configurable document analysis
259
+ - ✅ **Performance**: Rate limiting, input validation, and optimized processing
260
+
261
+ ## 📚 Architecture Highlights
262
+
263
+ ### **AI Integration**
264
+ - **Nebius AI**: Primary AI service for all language model tasks
265
+ - **DeepSeek Models**: State-of-the-art reasoning capabilities
266
+ - **Modal Integration**: Distributed processing for heavy workloads
267
+ - **Embedding Search**: Semantic similarity matching
268
+
269
+ ### **Data Flow**
270
+ 1. User query → AI query enhancement (optional)
271
+ 2. Parallel search: local storage + external sources
272
+ 3. URL validation and content verification
273
+ 4. Result ranking and relevance scoring
274
+ 5. AI-powered analysis and synthesis
275
+
276
+ ### **Component Architecture**
277
+ - **Enhanced Search Interface**: Unified search and AI tools
278
+ - **Knowledge Graph**: Interactive data visualization
279
+ - **Result Cards**: Rich content display with citations
280
+ - **Error Boundaries**: Resilient error handling
281
+
282
+ ## 🏆 Track 3: Agentic Demo Showcase Features
283
+
284
+ ### **🤖 "Show us the most incredible things that your agents can do!"**
285
+
286
+ KnowledgeBridge demonstrates sophisticated multi-agent systems in action:
287
+
288
+ ### **🧠 Autonomous Agent Workflows**
289
+ - **Smart Agent Coordination**: Multiple specialized agents work together to fulfill complex research tasks
290
+ - **Adaptive Agent Behavior**: Agents dynamically adjust strategies based on query complexity and source availability
291
+ - **Multi-Modal Agent Processing**: Different agent types (search, analysis, validation) collaborate seamlessly
292
+ - **Intelligent Agent Fallbacks**: Backup agents activate automatically when primary agents encounter issues
293
+
294
+ ### **🔍 Real-Time Agent Decision Making**
295
+ - **Query Analysis Agents**: Instantly determine optimal search strategies across 4+ sources
296
+ - **Load Balancing Agents**: Distribute workload intelligently based on API response times and rate limits
297
+ - **Quality Control Agents**: Evaluate and filter results in real-time for relevance and authenticity
298
+ - **Synthesis Agents**: Combine disparate information sources into coherent, actionable insights
299
+
300
+ ### **📊 Advanced Agent Orchestration**
301
+ - **Parallel Agent Execution**: Simultaneous deployment of search agents across GitHub, Wikipedia, ArXiv
302
+ - **Agent Communication Protocols**: Real-time coordination between agents for optimal resource utilization
303
+ - **Adaptive Agent Learning**: Agents improve performance based on user interactions and feedback
304
+ - **Error Recovery Agents**: Autonomous problem-solving when individual agents encounter failures
305
+
306
+ ### **🛡️ Production-Grade Agent Infrastructure**
307
+ - **Security Agent Monitoring**: Continuous protection against abuse with intelligent rate limiting
308
+ - **Validation Agent Networks**: Multi-layer content verification and URL authenticity checking
309
+ - **Performance Agent Optimization**: Automatic scaling and resource management for enterprise workloads
310
+ - **Resilience Agent Systems**: Graceful degradation and fault tolerance across all agent operations
311
+
312
+ ### **⚡ Agent Performance Metrics**
313
+ - **Sub-second Agent Response**: Query analysis and routing in <100ms
314
+ - **Concurrent Agent Processing**: 4+ agents working simultaneously on complex research tasks
315
+ - **Intelligent Agent Caching**: Smart result storage and retrieval for enhanced performance
316
+ - **Scalable Agent Architecture**: Horizontal scaling support for enterprise deployment
317
+
318
+ ## 📄 License
319
+
320
+ MIT License - see [LICENSE](LICENSE) file for details.
321
+
322
+ ## 🔗 Related Resources
323
+
324
+ - [Nebius AI Documentation](https://docs.nebius.ai/)
325
+ - [Modal Documentation](https://modal.com/docs)
326
+ - [React Query Documentation](https://tanstack.com/query/latest)
327
+ - [Radix UI Components](https://www.radix-ui.com/)
328
+
329
+ ---
330
+
331
+ ## 🚀 Agents-MCP-Hackathon Submission Summary
332
+
333
+ **KnowledgeBridge** showcases the incredible power of AI agents through:
334
+
335
+ 🤖 **Multi-Agent Orchestration** - Coordinated intelligence across search, analysis, and synthesis agents
336
+ 🔍 **Real-Time Decision Making** - Agents adapt strategies and optimize performance dynamically
337
+ 📊 **Advanced Agent Workflows** - Complex multi-step processes handled autonomously
338
+ 🛡️ **Production-Ready Agent Infrastructure** - Enterprise-grade security and resilience
339
+
340
+ **Track 3: Agentic Demo Showcase** - Demonstrating what happens when sophisticated AI agents work together to revolutionize knowledge discovery and research workflows.
341
+
342
+ **Built for the Hugging Face Agents-MCP-Hackathon** 🏆
343
+
344
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ KnowledgeBridge - AI-Enhanced Knowledge Discovery Platform
3
+ Hugging Face Spaces compatibility layer
4
+ """
5
+
6
+ import os
7
+ import subprocess
8
+ import sys
9
+
10
+ def main():
11
+ """
12
+ This file exists for Hugging Face Spaces compatibility.
13
+ The actual application runs via Docker and the Node.js server.
14
+ """
15
+ print("🚀 KnowledgeBridge is running via Docker on port 7860")
16
+ print("Visit the app interface above\!")
17
+ print("\n📚 Features:")
18
+ print("- AI-Enhanced Search with Nebius DeepSeek")
19
+ print("- Multi-source search (GitHub, Wikipedia, ArXiv)")
20
+ print("- Interactive Knowledge Graphs")
21
+ print("- Document Analysis with configurable output")
22
+ print("- Enterprise-grade security")
23
+ print("\n🔗 Built for Agents-MCP-Hackathon")
24
+
25
+ if __name__ == "__main__":
26
+ main()
27
+ EOF < /dev/null
client/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
6
+ </head>
7
+ <body>
8
+ <div id="root"></div>
9
+ <script type="module" src="/src/main.tsx"></script>
10
+ <!-- This is a replit script which adds a banner on the top of the page when opened in development mode outside the replit environment -->
11
+ <script type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js"></script>
12
+ </body>
13
+ </html>
client/src/App.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Switch, Route } from "wouter";
2
+ import { queryClient } from "./lib/queryClient";
3
+ import { QueryClientProvider } from "@tanstack/react-query";
4
+ import { Toaster } from "@/components/ui/toaster";
5
+ import { TooltipProvider } from "@/components/ui/tooltip";
6
+ import { ThemeProvider } from "@/components/theme-provider";
7
+ import ErrorBoundary from "@/components/ErrorBoundary";
8
+ import KnowledgeBase from "@/pages/knowledge-base";
9
+ import NotFound from "@/pages/not-found";
10
+
11
+ function Router() {
12
+ return (
13
+ <Switch>
14
+ <Route path="/" component={KnowledgeBase} />
15
+ <Route component={NotFound} />
16
+ </Switch>
17
+ );
18
+ }
19
+
20
+ function App() {
21
+ return (
22
+ <ErrorBoundary>
23
+ <ThemeProvider defaultTheme="light" storageKey="kb-browser-theme">
24
+ <QueryClientProvider client={queryClient}>
25
+ <TooltipProvider>
26
+ <Toaster />
27
+ <Router />
28
+ </TooltipProvider>
29
+ </QueryClientProvider>
30
+ </ThemeProvider>
31
+ </ErrorBoundary>
32
+ );
33
+ }
34
+
35
+ export default App;
client/src/components/ErrorBoundary.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
2
+
3
+ interface Props {
4
+ children: ReactNode;
5
+ }
6
+
7
+ interface State {
8
+ hasError: boolean;
9
+ error: Error | null;
10
+ }
11
+
12
+ class ErrorBoundary extends Component<Props, State> {
13
+ public state: State = {
14
+ hasError: false,
15
+ error: null
16
+ };
17
+
18
+ public static getDerivedStateFromError(error: Error): State {
19
+ return { hasError: true, error };
20
+ }
21
+
22
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
23
+ console.error('Error caught by boundary:', error, errorInfo);
24
+ }
25
+
26
+ public render() {
27
+ if (this.state.hasError) {
28
+ return (
29
+ <div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
30
+ <div className="max-w-md w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg p-6">
31
+ <div className="flex items-center">
32
+ <div className="flex-shrink-0">
33
+ <svg className="h-8 w-8 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
34
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
35
+ </svg>
36
+ </div>
37
+ <div className="ml-3">
38
+ <h3 className="text-sm font-medium text-gray-800 dark:text-gray-200">
39
+ Something went wrong
40
+ </h3>
41
+ <div className="mt-2 text-sm text-gray-500 dark:text-gray-400">
42
+ <p>The application encountered an unexpected error. Please refresh the page and try again.</p>
43
+ </div>
44
+ <div className="mt-4">
45
+ <button
46
+ onClick={() => window.location.reload()}
47
+ className="bg-red-100 hover:bg-red-200 dark:bg-red-900 dark:hover:bg-red-800 text-red-800 dark:text-red-200 px-4 py-2 rounded text-sm font-medium transition-colors"
48
+ >
49
+ Refresh Page
50
+ </button>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ {process.env.NODE_ENV === 'development' && this.state.error && (
55
+ <div className="mt-4 p-4 bg-gray-100 dark:bg-gray-700 rounded">
56
+ <details>
57
+ <summary className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer">
58
+ Error Details (Development)
59
+ </summary>
60
+ <pre className="mt-2 text-xs text-gray-600 dark:text-gray-400 overflow-auto">
61
+ {this.state.error.stack}
62
+ </pre>
63
+ </details>
64
+ </div>
65
+ )}
66
+ </div>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ return this.props.children;
72
+ }
73
+ }
74
+
75
+ export default ErrorBoundary;
client/src/components/knowledge-base/ai-assistant.tsx ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Input } from "@/components/ui/input";
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
8
+ import { Textarea } from "@/components/ui/textarea";
9
+ import {
10
+ Brain,
11
+ Sparkles,
12
+ FileText,
13
+ Search,
14
+ Loader2,
15
+ TrendingUp,
16
+ Lightbulb,
17
+ Target,
18
+ CheckCircle,
19
+ AlertCircle
20
+ } from "lucide-react";
21
+
22
+ interface AIAssistantProps {
23
+ onDocumentSelect?: (documentId: number) => void;
24
+ }
25
+
26
+ interface EnhancedSearchResult {
27
+ results: any[];
28
+ enhancedQuery?: {
29
+ enhancedQuery: string;
30
+ intent: string;
31
+ keywords: string[];
32
+ suggestions: string[];
33
+ };
34
+ searchInsights?: {
35
+ totalResults: number;
36
+ avgRelevanceScore: number;
37
+ modalResultsCount: number;
38
+ localResultsCount: number;
39
+ };
40
+ }
41
+
42
+ interface ResearchSynthesis {
43
+ synthesis: string;
44
+ keyFindings: string[];
45
+ gaps: string[];
46
+ recommendations: string[];
47
+ }
48
+
49
+ export default function AIAssistant({ onDocumentSelect }: AIAssistantProps) {
50
+ const [query, setQuery] = useState("");
51
+ const [selectedDocuments, setSelectedDocuments] = useState<number[]>([]);
52
+ const [analysisText, setAnalysisText] = useState("");
53
+ const queryClient = useQueryClient();
54
+
55
+ // Enhanced AI Search
56
+ const aiSearchMutation = useMutation({
57
+ mutationFn: async (searchQuery: string): Promise<EnhancedSearchResult> => {
58
+ const response = await fetch("/api/ai-search", {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify({
62
+ query: searchQuery,
63
+ maxResults: 10,
64
+ useQueryEnhancement: true
65
+ }),
66
+ });
67
+ if (!response.ok) throw new Error("Enhanced search failed");
68
+ return response.json();
69
+ },
70
+ onSuccess: () => {
71
+ queryClient.invalidateQueries({ queryKey: ["/api/search"] });
72
+ },
73
+ });
74
+
75
+ // Query Enhancement
76
+ const queryEnhancementMutation = useMutation({
77
+ mutationFn: async (originalQuery: string) => {
78
+ const response = await fetch("/api/enhance-query", {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify({ query: originalQuery }),
82
+ });
83
+ if (!response.ok) throw new Error("Query enhancement failed");
84
+ return response.json();
85
+ },
86
+ });
87
+
88
+ // Document Analysis
89
+ const documentAnalysisMutation = useMutation({
90
+ mutationFn: async ({ content, analysisType }: { content: string; analysisType: string }) => {
91
+ const response = await fetch("/api/analyze-document", {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: JSON.stringify({ content, analysisType }),
95
+ });
96
+ if (!response.ok) throw new Error("Document analysis failed");
97
+ return response.json();
98
+ },
99
+ });
100
+
101
+ // Research Synthesis
102
+ const researchSynthesisMutation = useMutation({
103
+ mutationFn: async ({ query, documentIds }: { query: string; documentIds: number[] }): Promise<ResearchSynthesis> => {
104
+ const response = await fetch("/api/research-synthesis", {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify({ query, documentIds }),
108
+ });
109
+ if (!response.ok) throw new Error("Research synthesis failed");
110
+ return response.json();
111
+ },
112
+ });
113
+
114
+ // Generate Embeddings
115
+ const embeddingsMutation = useMutation({
116
+ mutationFn: async (input: string) => {
117
+ const response = await fetch("/api/embeddings", {
118
+ method: "POST",
119
+ headers: { "Content-Type": "application/json" },
120
+ body: JSON.stringify({ input }),
121
+ });
122
+ if (!response.ok) throw new Error("Embedding generation failed");
123
+ return response.json();
124
+ },
125
+ });
126
+
127
+ const handleEnhancedSearch = () => {
128
+ if (!query.trim()) return;
129
+ aiSearchMutation.mutate(query);
130
+ };
131
+
132
+ const handleQueryEnhancement = () => {
133
+ if (!query.trim()) return;
134
+ queryEnhancementMutation.mutate(query);
135
+ };
136
+
137
+ const handleDocumentAnalysis = (analysisType: string) => {
138
+ if (!analysisText.trim()) return;
139
+ documentAnalysisMutation.mutate({ content: analysisText, analysisType });
140
+ };
141
+
142
+ const handleResearchSynthesis = () => {
143
+ if (!query.trim() || selectedDocuments.length === 0) return;
144
+ researchSynthesisMutation.mutate({ query, documentIds: selectedDocuments });
145
+ };
146
+
147
+ const handleGenerateEmbeddings = () => {
148
+ if (!query.trim()) return;
149
+ embeddingsMutation.mutate(query);
150
+ };
151
+
152
+ return (
153
+ <div className="space-y-6">
154
+ <Card className="border-gradient-to-r from-blue-200 to-purple-200 dark:from-blue-800 dark:to-purple-800">
155
+ <CardHeader>
156
+ <CardTitle className="flex items-center gap-2 text-xl">
157
+ <Brain className="w-6 h-6 text-blue-600" />
158
+ AI Research Assistant
159
+ <Badge variant="secondary" className="ml-2">Powered by Nebius & Modal</Badge>
160
+ </CardTitle>
161
+ </CardHeader>
162
+ <CardContent>
163
+ <Tabs defaultValue="search" className="w-full">
164
+ <TabsList className="grid grid-cols-4 w-full mb-6">
165
+ <TabsTrigger value="search" className="flex items-center gap-2">
166
+ <Search className="w-4 h-4" />
167
+ Smart Search
168
+ </TabsTrigger>
169
+ <TabsTrigger value="analysis" className="flex items-center gap-2">
170
+ <FileText className="w-4 h-4" />
171
+ Analysis
172
+ </TabsTrigger>
173
+ <TabsTrigger value="synthesis" className="flex items-center gap-2">
174
+ <Lightbulb className="w-4 h-4" />
175
+ Synthesis
176
+ </TabsTrigger>
177
+ <TabsTrigger value="embeddings" className="flex items-center gap-2">
178
+ <Sparkles className="w-4 h-4" />
179
+ Embeddings
180
+ </TabsTrigger>
181
+ </TabsList>
182
+
183
+ {/* Enhanced Search Tab */}
184
+ <TabsContent value="search" className="space-y-4">
185
+ <div className="space-y-3">
186
+ <div className="flex gap-2">
187
+ <Input
188
+ placeholder="Enter research query for AI-enhanced search..."
189
+ value={query}
190
+ onChange={(e) => setQuery(e.target.value)}
191
+ onKeyDown={(e) => e.key === "Enter" && handleEnhancedSearch()}
192
+ className="flex-1"
193
+ />
194
+ <Button
195
+ onClick={handleEnhancedSearch}
196
+ disabled={!query.trim() || aiSearchMutation.isPending}
197
+ >
198
+ {aiSearchMutation.isPending ? (
199
+ <Loader2 className="w-4 h-4 animate-spin" />
200
+ ) : (
201
+ <Search className="w-4 h-4" />
202
+ )}
203
+ </Button>
204
+ </div>
205
+
206
+ <div className="flex gap-2">
207
+ <Button
208
+ variant="outline"
209
+ size="sm"
210
+ onClick={handleQueryEnhancement}
211
+ disabled={!query.trim() || queryEnhancementMutation.isPending}
212
+ >
213
+ {queryEnhancementMutation.isPending ? (
214
+ <Loader2 className="w-3 h-3 animate-spin mr-1" />
215
+ ) : (
216
+ <Target className="w-3 h-3 mr-1" />
217
+ )}
218
+ Enhance Query
219
+ </Button>
220
+ </div>
221
+ </div>
222
+
223
+ {/* Query Enhancement Results */}
224
+ {queryEnhancementMutation.data && (
225
+ <Card className="bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
226
+ <CardContent className="pt-4">
227
+ <h4 className="font-semibold text-blue-900 dark:text-blue-100 mb-2">Enhanced Query</h4>
228
+ <p className="text-sm mb-3 font-mono bg-white dark:bg-gray-800 p-2 rounded">
229
+ {queryEnhancementMutation.data.enhancedQuery}
230
+ </p>
231
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
232
+ <div>
233
+ <span className="font-medium text-blue-800 dark:text-blue-200">Intent:</span>
234
+ <span className="ml-2">{queryEnhancementMutation.data.intent}</span>
235
+ </div>
236
+ <div>
237
+ <span className="font-medium text-blue-800 dark:text-blue-200">Keywords:</span>
238
+ <div className="flex flex-wrap gap-1 mt-1">
239
+ {queryEnhancementMutation.data.keywords.map((keyword: string, i: number) => (
240
+ <Badge key={i} variant="outline" className="text-xs">
241
+ {keyword}
242
+ </Badge>
243
+ ))}
244
+ </div>
245
+ </div>
246
+ </div>
247
+ </CardContent>
248
+ </Card>
249
+ )}
250
+
251
+ {/* Enhanced Search Results */}
252
+ {aiSearchMutation.data && (
253
+ <Card>
254
+ <CardHeader>
255
+ <CardTitle className="flex items-center gap-2 text-lg">
256
+ <TrendingUp className="w-5 h-5 text-green-600" />
257
+ AI-Enhanced Results
258
+ {aiSearchMutation.data.searchInsights && (
259
+ <Badge variant="secondary">
260
+ {aiSearchMutation.data.searchInsights.totalResults} results
261
+ </Badge>
262
+ )}
263
+ </CardTitle>
264
+ </CardHeader>
265
+ <CardContent className="space-y-3">
266
+ {aiSearchMutation.data.searchInsights && (
267
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg text-sm">
268
+ <div>
269
+ <span className="font-medium">Avg Relevance:</span>
270
+ <span className="ml-1 text-green-600">
271
+ {(aiSearchMutation.data.searchInsights.avgRelevanceScore * 100).toFixed(1)}%
272
+ </span>
273
+ </div>
274
+ <div>
275
+ <span className="font-medium">Modal Results:</span>
276
+ <span className="ml-1">{aiSearchMutation.data.searchInsights.modalResultsCount}</span>
277
+ </div>
278
+ <div>
279
+ <span className="font-medium">Local Results:</span>
280
+ <span className="ml-1">{aiSearchMutation.data.searchInsights.localResultsCount}</span>
281
+ </div>
282
+ <div>
283
+ <span className="font-medium">Total:</span>
284
+ <span className="ml-1">{aiSearchMutation.data.searchInsights.totalResults}</span>
285
+ </div>
286
+ </div>
287
+ )}
288
+
289
+ <div className="space-y-2 max-h-96 overflow-y-auto">
290
+ {aiSearchMutation.data.results.map((result: any, index: number) => (
291
+ <Card key={index} className="p-3 hover:bg-gray-50 dark:hover:bg-gray-800">
292
+ <div className="flex justify-between items-start mb-2">
293
+ <h5 className="font-medium text-sm">{result.title}</h5>
294
+ <div className="flex items-center gap-2">
295
+ {result.relevanceScore && (
296
+ <Badge variant="outline" className="text-xs">
297
+ {(result.relevanceScore * 100).toFixed(0)}%
298
+ </Badge>
299
+ )}
300
+ {result.aiExplanation && (
301
+ <CheckCircle className="w-4 h-4 text-green-500" />
302
+ )}
303
+ </div>
304
+ </div>
305
+ <p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
306
+ {result.snippet}
307
+ </p>
308
+ {result.keyReasons && (
309
+ <div className="text-xs">
310
+ <span className="font-medium">AI Analysis:</span>
311
+ <ul className="list-disc list-inside ml-2 mt-1">
312
+ {result.keyReasons.slice(0, 2).map((reason: string, i: number) => (
313
+ <li key={i} className="text-gray-600 dark:text-gray-400">{reason}</li>
314
+ ))}
315
+ </ul>
316
+ </div>
317
+ )}
318
+ </Card>
319
+ ))}
320
+ </div>
321
+ </CardContent>
322
+ </Card>
323
+ )}
324
+ </TabsContent>
325
+
326
+ {/* Document Analysis Tab */}
327
+ <TabsContent value="analysis" className="space-y-4">
328
+ <div className="space-y-3">
329
+ <Textarea
330
+ placeholder="Paste document content for AI analysis..."
331
+ value={analysisText}
332
+ onChange={(e) => setAnalysisText(e.target.value)}
333
+ className="min-h-32"
334
+ />
335
+ <div className="flex gap-2 flex-wrap">
336
+ {['summary', 'classification', 'key_points', 'quality_score'].map((type) => (
337
+ <Button
338
+ key={type}
339
+ variant="outline"
340
+ size="sm"
341
+ onClick={() => handleDocumentAnalysis(type)}
342
+ disabled={!analysisText.trim() || documentAnalysisMutation.isPending}
343
+ >
344
+ {documentAnalysisMutation.isPending ? (
345
+ <Loader2 className="w-3 h-3 animate-spin mr-1" />
346
+ ) : (
347
+ <FileText className="w-3 h-3 mr-1" />
348
+ )}
349
+ {type.replace('_', ' ').toUpperCase()}
350
+ </Button>
351
+ ))}
352
+ </div>
353
+ </div>
354
+
355
+ {documentAnalysisMutation.data && (
356
+ <Card className="bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
357
+ <CardHeader>
358
+ <CardTitle className="text-lg flex items-center gap-2">
359
+ <CheckCircle className="w-5 h-5 text-green-600" />
360
+ Analysis Result
361
+ </CardTitle>
362
+ </CardHeader>
363
+ <CardContent>
364
+ <div className="whitespace-pre-wrap text-sm">
365
+ {documentAnalysisMutation.data.analysis}
366
+ </div>
367
+ </CardContent>
368
+ </Card>
369
+ )}
370
+ </TabsContent>
371
+
372
+ {/* Research Synthesis Tab */}
373
+ <TabsContent value="synthesis" className="space-y-4">
374
+ <div className="space-y-3">
375
+ <Input
376
+ placeholder="Research question for synthesis..."
377
+ value={query}
378
+ onChange={(e) => setQuery(e.target.value)}
379
+ />
380
+ <div className="flex items-center gap-2">
381
+ <span className="text-sm font-medium">Selected Documents:</span>
382
+ <Badge variant="outline">{selectedDocuments.length}</Badge>
383
+ <Button
384
+ size="sm"
385
+ variant="outline"
386
+ onClick={() => setSelectedDocuments([])}
387
+ >
388
+ Clear
389
+ </Button>
390
+ </div>
391
+ <Button
392
+ onClick={handleResearchSynthesis}
393
+ disabled={!query.trim() || selectedDocuments.length === 0 || researchSynthesisMutation.isPending}
394
+ className="w-full"
395
+ >
396
+ {researchSynthesisMutation.isPending ? (
397
+ <Loader2 className="w-4 h-4 animate-spin mr-2" />
398
+ ) : (
399
+ <Lightbulb className="w-4 h-4 mr-2" />
400
+ )}
401
+ Generate Research Synthesis
402
+ </Button>
403
+ </div>
404
+
405
+ {researchSynthesisMutation.data && (
406
+ <Card className="bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800">
407
+ <CardContent className="pt-4 space-y-4">
408
+ <div>
409
+ <h4 className="font-semibold text-purple-900 dark:text-purple-100 mb-2">Synthesis</h4>
410
+ <p className="text-sm">{researchSynthesisMutation.data.synthesis}</p>
411
+ </div>
412
+
413
+ {researchSynthesisMutation.data.keyFindings.length > 0 && (
414
+ <div>
415
+ <h4 className="font-semibold text-purple-900 dark:text-purple-100 mb-2">Key Findings</h4>
416
+ <ul className="list-disc list-inside text-sm space-y-1">
417
+ {researchSynthesisMutation.data.keyFindings.map((finding: string, i: number) => (
418
+ <li key={i}>{finding}</li>
419
+ ))}
420
+ </ul>
421
+ </div>
422
+ )}
423
+
424
+ {researchSynthesisMutation.data.recommendations.length > 0 && (
425
+ <div>
426
+ <h4 className="font-semibold text-purple-900 dark:text-purple-100 mb-2">Recommendations</h4>
427
+ <ul className="list-disc list-inside text-sm space-y-1">
428
+ {researchSynthesisMutation.data.recommendations.map((rec: string, i: number) => (
429
+ <li key={i}>{rec}</li>
430
+ ))}
431
+ </ul>
432
+ </div>
433
+ )}
434
+ </CardContent>
435
+ </Card>
436
+ )}
437
+ </TabsContent>
438
+
439
+ {/* Embeddings Tab */}
440
+ <TabsContent value="embeddings" className="space-y-4">
441
+ <div className="space-y-3">
442
+ <Input
443
+ placeholder="Text to generate embeddings..."
444
+ value={query}
445
+ onChange={(e) => setQuery(e.target.value)}
446
+ />
447
+ <Button
448
+ onClick={handleGenerateEmbeddings}
449
+ disabled={!query.trim() || embeddingsMutation.isPending}
450
+ className="w-full"
451
+ >
452
+ {embeddingsMutation.isPending ? (
453
+ <Loader2 className="w-4 h-4 animate-spin mr-2" />
454
+ ) : (
455
+ <Sparkles className="w-4 h-4 mr-2" />
456
+ )}
457
+ Generate Embeddings with Nebius
458
+ </Button>
459
+ </div>
460
+
461
+ {embeddingsMutation.data && (
462
+ <Card className="bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
463
+ <CardContent className="pt-4 space-y-3">
464
+ <div className="grid grid-cols-2 gap-4 text-sm">
465
+ <div>
466
+ <span className="font-medium">Model:</span>
467
+ <span className="ml-2">{embeddingsMutation.data.model}</span>
468
+ </div>
469
+ <div>
470
+ <span className="font-medium">Dimensions:</span>
471
+ <span className="ml-2">{embeddingsMutation.data.data[0].embedding.length}</span>
472
+ </div>
473
+ </div>
474
+ <div>
475
+ <span className="font-medium text-sm">Vector (first 10 dimensions):</span>
476
+ <div className="font-mono text-xs bg-white dark:bg-gray-800 p-2 rounded mt-1 overflow-x-auto">
477
+ [{embeddingsMutation.data.data[0].embedding.slice(0, 10).map((val: number) => val.toFixed(4)).join(', ')}...]
478
+ </div>
479
+ </div>
480
+ <div className="text-xs text-gray-600 dark:text-gray-400">
481
+ Token usage: {embeddingsMutation.data.usage.total_tokens} tokens
482
+ </div>
483
+ </CardContent>
484
+ </Card>
485
+ )}
486
+ </TabsContent>
487
+ </Tabs>
488
+ </CardContent>
489
+ </Card>
490
+
491
+ {/* Error States */}
492
+ {(aiSearchMutation.error || documentAnalysisMutation.error || researchSynthesisMutation.error || embeddingsMutation.error) && (
493
+ <Card className="border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20">
494
+ <CardContent className="pt-4">
495
+ <div className="flex items-center gap-2 text-red-700 dark:text-red-300">
496
+ <AlertCircle className="w-4 h-4" />
497
+ <span className="font-medium">Error occurred</span>
498
+ </div>
499
+ <p className="text-sm text-red-600 dark:text-red-400 mt-1">
500
+ {(aiSearchMutation.error || documentAnalysisMutation.error || researchSynthesisMutation.error || embeddingsMutation.error)?.message}
501
+ </p>
502
+ </CardContent>
503
+ </Card>
504
+ )}
505
+ </div>
506
+ );
507
+ }
client/src/components/knowledge-base/citation-panel.tsx ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { X, Download, Trash2 } from "lucide-react";
4
+ import { type Citation } from "@shared/schema";
5
+
6
+ interface CitationPanelProps {
7
+ citations: Citation[];
8
+ isVisible: boolean;
9
+ onClose: () => void;
10
+ onRemoveCitation: (citationId: number) => void;
11
+ }
12
+
13
+ export default function CitationPanel({
14
+ citations,
15
+ isVisible,
16
+ onClose,
17
+ onRemoveCitation
18
+ }: CitationPanelProps) {
19
+ if (!isVisible) return null;
20
+
21
+ const handleExportCitations = () => {
22
+ const citationText = citations
23
+ .map((citation, index) => {
24
+ return `[${index + 1}] ${citation.citationText}${citation.section ? ` (${citation.section})` : ''}${citation.pageNumber ? ` - Page ${citation.pageNumber}` : ''}`;
25
+ })
26
+ .join('\n\n');
27
+
28
+ const blob = new Blob([citationText], { type: 'text/plain' });
29
+ const url = URL.createObjectURL(blob);
30
+ const a = document.createElement('a');
31
+ a.href = url;
32
+ a.download = 'citations.txt';
33
+ document.body.appendChild(a);
34
+ a.click();
35
+ document.body.removeChild(a);
36
+ URL.revokeObjectURL(url);
37
+ };
38
+
39
+ return (
40
+ <div className="fixed bottom-6 right-6 z-50">
41
+ <div className="bg-white rounded-xl shadow-lg border border-slate-200 w-80 max-h-96 flex flex-col">
42
+ {/* Header */}
43
+ <div className="flex items-center justify-between p-4 border-b border-slate-200">
44
+ <div className="flex items-center gap-2">
45
+ <h4 className="font-medium text-slate-900">Active Citations</h4>
46
+ <Badge variant="secondary" className="text-xs">
47
+ {citations.length}
48
+ </Badge>
49
+ </div>
50
+ <Button
51
+ variant="ghost"
52
+ size="sm"
53
+ onClick={onClose}
54
+ className="text-slate-400 hover:text-slate-600 p-1"
55
+ >
56
+ <X className="w-4 h-4" />
57
+ </Button>
58
+ </div>
59
+
60
+ {/* Citations List */}
61
+ <div className="flex-1 overflow-y-auto p-4">
62
+ {citations.length === 0 ? (
63
+ <div className="text-center py-8">
64
+ <div className="text-slate-400 mb-2">
65
+ <svg className="w-8 h-8 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
66
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.447-.935-6-2.45" />
67
+ </svg>
68
+ </div>
69
+ <p className="text-sm text-slate-500">No citations added yet</p>
70
+ <p className="text-xs text-slate-400 mt-1">
71
+ Click "Add Citation" on search results to build your reference list
72
+ </p>
73
+ </div>
74
+ ) : (
75
+ <div className="space-y-3">
76
+ {citations.map((citation, index) => (
77
+ <div
78
+ key={citation.id}
79
+ className="group flex items-start gap-3 p-3 bg-slate-50 rounded-lg hover:bg-slate-100 transition-colors"
80
+ >
81
+ <span className="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-medium flex-shrink-0 mt-0.5">
82
+ {index + 1}
83
+ </span>
84
+ <div className="flex-1 min-w-0">
85
+ <p className="text-sm text-slate-700 leading-relaxed line-clamp-3">
86
+ {citation.citationText}
87
+ </p>
88
+ {(citation.section || citation.pageNumber) && (
89
+ <div className="flex items-center gap-2 mt-2">
90
+ {citation.section && (
91
+ <span className="text-xs text-slate-500 bg-white px-2 py-1 rounded">
92
+ {citation.section}
93
+ </span>
94
+ )}
95
+ {citation.pageNumber && (
96
+ <span className="text-xs text-slate-500 bg-white px-2 py-1 rounded">
97
+ Page {citation.pageNumber}
98
+ </span>
99
+ )}
100
+ </div>
101
+ )}
102
+ </div>
103
+ <Button
104
+ variant="ghost"
105
+ size="sm"
106
+ onClick={() => onRemoveCitation(citation.id)}
107
+ className="opacity-0 group-hover:opacity-100 transition-opacity text-slate-400 hover:text-red-600 p-1"
108
+ >
109
+ <Trash2 className="w-3 h-3" />
110
+ </Button>
111
+ </div>
112
+ ))}
113
+ </div>
114
+ )}
115
+ </div>
116
+
117
+ {/* Footer */}
118
+ {citations.length > 0 && (
119
+ <div className="p-4 border-t border-slate-200">
120
+ <Button
121
+ onClick={handleExportCitations}
122
+ className="w-full bg-slate-100 text-slate-700 hover:bg-slate-200"
123
+ size="sm"
124
+ >
125
+ <Download className="w-4 h-4 mr-2" />
126
+ Export Citations
127
+ </Button>
128
+ </div>
129
+ )}
130
+ </div>
131
+ </div>
132
+ );
133
+ }
client/src/components/knowledge-base/enhanced-search-interface.tsx ADDED
@@ -0,0 +1,603 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { useMutation } from "@tanstack/react-query";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Input } from "@/components/ui/input";
5
+ import { Label } from "@/components/ui/label";
6
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
7
+ import { Checkbox } from "@/components/ui/checkbox";
8
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
9
+ import { Badge } from "@/components/ui/badge";
10
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
11
+ import { Textarea } from "@/components/ui/textarea";
12
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
13
+ import {
14
+ Search,
15
+ Loader2,
16
+ Brain,
17
+ Sparkles,
18
+ Target,
19
+ FileText,
20
+ Lightbulb,
21
+ ChevronDown,
22
+ ChevronUp,
23
+ Wand2,
24
+ AlertCircle
25
+ } from "lucide-react";
26
+ import { type SearchRequest } from "@shared/schema";
27
+
28
+ interface SearchInterfaceProps {
29
+ onSearch: (request: SearchRequest) => void;
30
+ onAISearch?: (query: string) => void;
31
+ isLoading?: boolean;
32
+ onDocumentSelect?: (documentId: number) => void;
33
+ }
34
+
35
+ interface EnhancedSearchResult {
36
+ results: any[];
37
+ enhancedQuery?: {
38
+ enhancedQuery: string;
39
+ intent: string;
40
+ keywords: string[];
41
+ suggestions: string[];
42
+ };
43
+ searchInsights?: {
44
+ totalResults: number;
45
+ avgRelevanceScore: number;
46
+ modalResultsCount: number;
47
+ localResultsCount: number;
48
+ };
49
+ }
50
+
51
+ export default function EnhancedSearchInterface({ onSearch, onAISearch, isLoading, onDocumentSelect }: SearchInterfaceProps) {
52
+ const [query, setQuery] = useState("");
53
+ const [searchType, setSearchType] = useState<"semantic" | "keyword" | "hybrid">("semantic");
54
+ const [sourceTypes, setSourceTypes] = useState<string[]>(["pdf", "web", "academic", "code"]);
55
+ const [showAITools, setShowAITools] = useState(false);
56
+ const [analysisText, setAnalysisText] = useState("");
57
+ const [selectedDocuments, setSelectedDocuments] = useState<number[]>([]);
58
+ const [useMarkdown, setUseMarkdown] = useState(true);
59
+
60
+ const handleSubmit = (e: React.FormEvent) => {
61
+ e.preventDefault();
62
+ if (!query.trim()) return;
63
+
64
+ onSearch({
65
+ query: query.trim(),
66
+ searchType,
67
+ filters: {
68
+ sourceTypes: sourceTypes.length > 0 ? sourceTypes : undefined,
69
+ },
70
+ limit: 10,
71
+ offset: 0,
72
+ });
73
+ };
74
+
75
+ const handleSourceTypeChange = (sourceType: string, checked: boolean) => {
76
+ setSourceTypes(prev =>
77
+ checked
78
+ ? [...prev, sourceType]
79
+ : prev.filter(type => type !== sourceType)
80
+ );
81
+ };
82
+
83
+ const handleKeyDown = (e: React.KeyboardEvent) => {
84
+ if (e.key === "Enter" && !e.shiftKey) {
85
+ e.preventDefault();
86
+ handleSubmit(e);
87
+ } else if (e.key === "Escape") {
88
+ setQuery("");
89
+ }
90
+ };
91
+
92
+ // Enhanced AI Search
93
+ const aiSearchMutation = useMutation({
94
+ mutationFn: async (searchQuery: string): Promise<EnhancedSearchResult> => {
95
+ const response = await fetch("/api/ai-search", {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({
99
+ query: searchQuery,
100
+ maxResults: 10,
101
+ useQueryEnhancement: true
102
+ }),
103
+ });
104
+ if (!response.ok) throw new Error("Enhanced search failed");
105
+ return response.json();
106
+ },
107
+ });
108
+
109
+ // Query Enhancement
110
+ const queryEnhancementMutation = useMutation({
111
+ mutationFn: async (originalQuery: string) => {
112
+ const response = await fetch("/api/enhance-query", {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify({ query: originalQuery }),
116
+ });
117
+ if (!response.ok) throw new Error("Query enhancement failed");
118
+ return response.json();
119
+ },
120
+ });
121
+
122
+ // Document Analysis
123
+ const documentAnalysisMutation = useMutation({
124
+ mutationFn: async ({ content, analysisType, useMarkdown }: { content: string; analysisType: string; useMarkdown?: boolean }) => {
125
+ const response = await fetch("/api/analyze-document", {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify({ content, analysisType, useMarkdown }),
129
+ });
130
+ if (!response.ok) throw new Error("Document analysis failed");
131
+ return response.json();
132
+ },
133
+ });
134
+
135
+ // Generate Embeddings
136
+ const embeddingsMutation = useMutation({
137
+ mutationFn: async (input: string) => {
138
+ const response = await fetch("/api/embeddings", {
139
+ method: "POST",
140
+ headers: { "Content-Type": "application/json" },
141
+ body: JSON.stringify({ input }),
142
+ });
143
+ if (!response.ok) throw new Error("Embedding generation failed");
144
+ return response.json();
145
+ },
146
+ });
147
+
148
+ const handleEnhancedSearch = () => {
149
+ if (!query.trim()) return;
150
+ aiSearchMutation.mutate(query);
151
+ if (onAISearch) onAISearch(query);
152
+ };
153
+
154
+ const handleQueryEnhancement = () => {
155
+ if (!query.trim()) return;
156
+ queryEnhancementMutation.mutate(query);
157
+ };
158
+
159
+ const handleDocumentAnalysis = (analysisType: string) => {
160
+ if (!analysisText.trim()) return;
161
+ documentAnalysisMutation.mutate({
162
+ content: analysisText,
163
+ analysisType,
164
+ useMarkdown
165
+ });
166
+ };
167
+
168
+ const handleGenerateEmbeddings = () => {
169
+ if (!query.trim()) return;
170
+ embeddingsMutation.mutate(query);
171
+ };
172
+
173
+ const applyEnhancedQuery = (enhancedQuery: string) => {
174
+ setQuery(enhancedQuery);
175
+ onSearch({
176
+ query: enhancedQuery,
177
+ searchType,
178
+ filters: {
179
+ sourceTypes: sourceTypes.length > 0 ? sourceTypes : undefined,
180
+ },
181
+ limit: 10,
182
+ offset: 0,
183
+ });
184
+ };
185
+
186
+ return (
187
+ <div className="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-6 mb-6">
188
+ <div className="flex items-center justify-between mb-4">
189
+ <div className="flex items-center gap-2">
190
+ <Brain className="w-5 h-5 text-blue-600" />
191
+ <h2 className="text-lg font-semibold text-slate-900 dark:text-slate-100">AI-Enhanced Search</h2>
192
+ <Badge variant="secondary" className="text-xs">Powered by Nebius & Modal</Badge>
193
+ </div>
194
+ <Button
195
+ type="button"
196
+ variant="outline"
197
+ size="sm"
198
+ onClick={() => setShowAITools(!showAITools)}
199
+ className="flex items-center gap-1"
200
+ >
201
+ <Wand2 className="w-4 h-4" />
202
+ AI Tools
203
+ {showAITools ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
204
+ </Button>
205
+ </div>
206
+
207
+ <form onSubmit={handleSubmit}>
208
+ <div className="flex flex-col lg:flex-row gap-4">
209
+ <div className="flex-1">
210
+ <Label htmlFor="knowledge-search" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
211
+ Search Knowledge Base
212
+ </Label>
213
+ <div className="relative">
214
+ <Input
215
+ id="knowledge-search"
216
+ type="text"
217
+ placeholder="Enter your query for AI-enhanced search... (Press Enter to search, Esc to clear)"
218
+ value={query}
219
+ onChange={(e) => setQuery(e.target.value)}
220
+ onKeyDown={handleKeyDown}
221
+ className="pl-11 pr-12 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
222
+ disabled={isLoading}
223
+ aria-label="Search knowledge base"
224
+ />
225
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
226
+ {query && (
227
+ <Button
228
+ type="button"
229
+ variant="ghost"
230
+ size="sm"
231
+ onClick={handleQueryEnhancement}
232
+ disabled={queryEnhancementMutation.isPending}
233
+ className="absolute right-2 top-1/2 transform -translate-y-1/2 h-8 w-8 p-0"
234
+ title="Enhance query with AI"
235
+ >
236
+ {queryEnhancementMutation.isPending ? (
237
+ <Loader2 className="w-3 h-3 animate-spin" />
238
+ ) : (
239
+ <Sparkles className="w-3 h-3 text-purple-500" />
240
+ )}
241
+ </Button>
242
+ )}
243
+ </div>
244
+ </div>
245
+
246
+ <div className="lg:w-auto">
247
+ <Label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
248
+ Search Type
249
+ </Label>
250
+ <Select value={searchType} onValueChange={(value: any) => setSearchType(value)}>
251
+ <SelectTrigger className="w-full lg:w-40">
252
+ <SelectValue />
253
+ </SelectTrigger>
254
+ <SelectContent>
255
+ <SelectItem value="semantic">Semantic Search</SelectItem>
256
+ <SelectItem value="keyword">Keyword Search</SelectItem>
257
+ <SelectItem value="hybrid">Hybrid Search</SelectItem>
258
+ </SelectContent>
259
+ </Select>
260
+ </div>
261
+
262
+ <div className="lg:w-auto flex items-end gap-2">
263
+ <Button
264
+ type="submit"
265
+ disabled={!query.trim() || isLoading}
266
+ className="px-6 py-3 bg-blue-600 hover:bg-blue-700 focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
267
+ aria-label={isLoading ? "Searching knowledge base" : "Search knowledge base"}
268
+ >
269
+ {isLoading ? (
270
+ <>
271
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" aria-hidden="true" />
272
+ Searching...
273
+ </>
274
+ ) : (
275
+ <>
276
+ <Search className="w-4 h-4 mr-2" aria-hidden="true" />
277
+ Search
278
+ </>
279
+ )}
280
+ </Button>
281
+ <Button
282
+ type="button"
283
+ variant="outline"
284
+ onClick={handleEnhancedSearch}
285
+ disabled={!query.trim() || aiSearchMutation.isPending}
286
+ className="px-4 py-3 border-purple-300 text-purple-700 hover:bg-purple-50 dark:border-purple-600 dark:text-purple-300 dark:hover:bg-purple-900/20"
287
+ title="AI-Enhanced Search"
288
+ >
289
+ {aiSearchMutation.isPending ? (
290
+ <Loader2 className="w-4 h-4 animate-spin" />
291
+ ) : (
292
+ <Brain className="w-4 h-4" />
293
+ )}
294
+ </Button>
295
+ </div>
296
+ </div>
297
+
298
+ {/* Search Filters */}
299
+ <div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
300
+ <div className="flex flex-wrap gap-6">
301
+ {[
302
+ { id: "pdf", label: "PDFs" },
303
+ { id: "web", label: "Web Pages" },
304
+ { id: "academic", label: "Academic Papers" },
305
+ { id: "code", label: "Code Repositories" }
306
+ ].map(({ id, label }) => (
307
+ <div key={id} className="flex items-center space-x-2">
308
+ <Checkbox
309
+ id={`filter-${id}`}
310
+ checked={sourceTypes.includes(id)}
311
+ onCheckedChange={(checked) => handleSourceTypeChange(id, !!checked)}
312
+ />
313
+ <Label
314
+ htmlFor={`filter-${id}`}
315
+ className="text-sm text-slate-600 dark:text-slate-400 cursor-pointer"
316
+ >
317
+ {label}
318
+ </Label>
319
+ </div>
320
+ ))}
321
+ </div>
322
+ </div>
323
+ </form>
324
+
325
+ {/* Query Enhancement Results */}
326
+ {queryEnhancementMutation.data && (
327
+ <div className="mt-4">
328
+ <Card className="bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800">
329
+ <CardContent className="pt-4">
330
+ <div className="flex items-center justify-between mb-3">
331
+ <h4 className="font-semibold text-purple-900 dark:text-purple-100 flex items-center gap-2">
332
+ <Sparkles className="w-4 h-4" />
333
+ Enhanced Query Suggestion
334
+ </h4>
335
+ <Button
336
+ size="sm"
337
+ onClick={() => applyEnhancedQuery(queryEnhancementMutation.data.enhancedQuery)}
338
+ className="bg-purple-600 hover:bg-purple-700"
339
+ >
340
+ Use This Query
341
+ </Button>
342
+ </div>
343
+ <p className="text-sm mb-3 font-mono bg-white dark:bg-gray-800 p-3 rounded border">
344
+ {queryEnhancementMutation.data.enhancedQuery}
345
+ </p>
346
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
347
+ <div>
348
+ <span className="font-medium text-purple-800 dark:text-purple-200">Intent:</span>
349
+ <span className="ml-2">{queryEnhancementMutation.data.intent}</span>
350
+ </div>
351
+ <div>
352
+ <span className="font-medium text-purple-800 dark:text-purple-200">Keywords:</span>
353
+ <div className="flex flex-wrap gap-1 mt-1">
354
+ {queryEnhancementMutation.data.keywords.map((keyword: string, i: number) => (
355
+ <Badge key={i} variant="outline" className="text-xs">
356
+ {keyword}
357
+ </Badge>
358
+ ))}
359
+ </div>
360
+ </div>
361
+ </div>
362
+ </CardContent>
363
+ </Card>
364
+ </div>
365
+ )}
366
+
367
+ {/* AI Enhanced Search Results */}
368
+ {aiSearchMutation.data && (
369
+ <div className="mt-4">
370
+ <Card className="bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
371
+ <CardHeader className="pb-3">
372
+ <CardTitle className="flex items-center gap-2 text-lg">
373
+ <Brain className="w-5 h-5 text-green-600" />
374
+ AI-Enhanced Results
375
+ {aiSearchMutation.data.searchInsights && (
376
+ <Badge variant="secondary">
377
+ {aiSearchMutation.data.searchInsights.totalResults} results
378
+ </Badge>
379
+ )}
380
+ </CardTitle>
381
+ </CardHeader>
382
+ <CardContent className="space-y-3">
383
+ {aiSearchMutation.data.searchInsights && (
384
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg text-sm">
385
+ <div>
386
+ <span className="font-medium">Avg Relevance:</span>
387
+ <span className="ml-1 text-green-600">
388
+ {(aiSearchMutation.data.searchInsights.avgRelevanceScore * 100).toFixed(1)}%
389
+ </span>
390
+ </div>
391
+ <div>
392
+ <span className="font-medium">Modal Results:</span>
393
+ <span className="ml-1">{aiSearchMutation.data.searchInsights.modalResultsCount}</span>
394
+ </div>
395
+ <div>
396
+ <span className="font-medium">Local Results:</span>
397
+ <span className="ml-1">{aiSearchMutation.data.searchInsights.localResultsCount}</span>
398
+ </div>
399
+ <div>
400
+ <span className="font-medium">Total:</span>
401
+ <span className="ml-1">{aiSearchMutation.data.searchInsights.totalResults}</span>
402
+ </div>
403
+ </div>
404
+ )}
405
+ <div className="text-sm text-green-700 dark:text-green-300">
406
+ ✨ AI-enhanced search completed. Results are ranked by semantic relevance and include additional context.
407
+ </div>
408
+ </CardContent>
409
+ </Card>
410
+ </div>
411
+ )}
412
+
413
+ {/* Collapsible AI Tools */}
414
+ <Collapsible open={showAITools} onOpenChange={setShowAITools}>
415
+ <CollapsibleContent className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
416
+ <Tabs defaultValue="analysis" className="w-full">
417
+ <TabsList className="grid grid-cols-3 w-full mb-4">
418
+ <TabsTrigger value="analysis" className="flex items-center gap-2">
419
+ <FileText className="w-4 h-4" />
420
+ Analysis
421
+ </TabsTrigger>
422
+ <TabsTrigger value="embeddings" className="flex items-center gap-2">
423
+ <Sparkles className="w-4 h-4" />
424
+ Embeddings
425
+ </TabsTrigger>
426
+ <TabsTrigger value="tools" className="flex items-center gap-2">
427
+ <Target className="w-4 h-4" />
428
+ External Tools
429
+ </TabsTrigger>
430
+ </TabsList>
431
+
432
+ {/* Document Analysis Tab */}
433
+ <TabsContent value="analysis" className="space-y-4">
434
+ <div className="space-y-3">
435
+ <Label className="text-sm font-medium">Document Analysis</Label>
436
+ <Textarea
437
+ placeholder="Paste document content for AI analysis..."
438
+ value={analysisText}
439
+ onChange={(e) => setAnalysisText(e.target.value)}
440
+ className="min-h-24"
441
+ />
442
+
443
+ {/* Formatting Option */}
444
+ <div className="flex items-center space-x-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
445
+ <Checkbox
446
+ id="use-markdown"
447
+ checked={useMarkdown}
448
+ onCheckedChange={(checked) => setUseMarkdown(!!checked)}
449
+ />
450
+ <Label htmlFor="use-markdown" className="text-sm cursor-pointer">
451
+ Use markdown formatting (**bold**, bullet points, etc.)
452
+ </Label>
453
+ </div>
454
+
455
+ <div className="flex gap-2 flex-wrap">
456
+ {['summary', 'classification', 'key_points', 'quality_score'].map((type) => (
457
+ <Button
458
+ key={type}
459
+ variant="outline"
460
+ size="sm"
461
+ onClick={() => handleDocumentAnalysis(type)}
462
+ disabled={!analysisText.trim() || documentAnalysisMutation.isPending}
463
+ >
464
+ {documentAnalysisMutation.isPending ? (
465
+ <Loader2 className="w-3 h-3 animate-spin mr-1" />
466
+ ) : (
467
+ <FileText className="w-3 h-3 mr-1" />
468
+ )}
469
+ {type.replace('_', ' ').toUpperCase()}
470
+ </Button>
471
+ ))}
472
+ </div>
473
+ </div>
474
+
475
+ {documentAnalysisMutation.data && (
476
+ <Card className="bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
477
+ <CardHeader>
478
+ <CardTitle className="text-lg flex items-center gap-2">
479
+ <FileText className="w-5 h-5 text-blue-600" />
480
+ Analysis Result
481
+ </CardTitle>
482
+ </CardHeader>
483
+ <CardContent>
484
+ <div className="whitespace-pre-wrap text-sm">
485
+ {documentAnalysisMutation.data.analysis}
486
+ </div>
487
+ </CardContent>
488
+ </Card>
489
+ )}
490
+ </TabsContent>
491
+
492
+ {/* Embeddings Tab */}
493
+ <TabsContent value="embeddings" className="space-y-4">
494
+ <div className="space-y-3">
495
+ <Label className="text-sm font-medium">Generate Embeddings</Label>
496
+ <div className="flex gap-2">
497
+ <Input
498
+ placeholder="Text to generate embeddings..."
499
+ value={query}
500
+ onChange={(e) => setQuery(e.target.value)}
501
+ className="flex-1"
502
+ />
503
+ <Button
504
+ onClick={handleGenerateEmbeddings}
505
+ disabled={!query.trim() || embeddingsMutation.isPending}
506
+ >
507
+ {embeddingsMutation.isPending ? (
508
+ <Loader2 className="w-4 h-4 animate-spin" />
509
+ ) : (
510
+ <Sparkles className="w-4 h-4" />
511
+ )}
512
+ </Button>
513
+ </div>
514
+ </div>
515
+
516
+ {embeddingsMutation.data && (
517
+ <Card className="bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
518
+ <CardContent className="pt-4 space-y-3">
519
+ <div className="grid grid-cols-2 gap-4 text-sm">
520
+ <div>
521
+ <span className="font-medium">Model:</span>
522
+ <span className="ml-2">{embeddingsMutation.data.model}</span>
523
+ </div>
524
+ <div>
525
+ <span className="font-medium">Dimensions:</span>
526
+ <span className="ml-2">{embeddingsMutation.data.data[0].embedding.length}</span>
527
+ </div>
528
+ </div>
529
+ <div>
530
+ <span className="font-medium text-sm">Vector (first 10 dimensions):</span>
531
+ <div className="font-mono text-xs bg-white dark:bg-gray-800 p-2 rounded mt-1 overflow-x-auto">
532
+ [{embeddingsMutation.data.data[0].embedding.slice(0, 10).map((val: number) => val.toFixed(4)).join(', ')}...]
533
+ </div>
534
+ </div>
535
+ </CardContent>
536
+ </Card>
537
+ )}
538
+ </TabsContent>
539
+
540
+ {/* External Tools Tab */}
541
+ <TabsContent value="tools" className="space-y-4">
542
+ <div>
543
+ <Label className="text-sm font-medium mb-3 block">AI Development Platforms</Label>
544
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
545
+ <Button
546
+ variant="outline"
547
+ className="h-auto p-4 hover:bg-blue-50 hover:border-blue-300 dark:hover:bg-blue-900/20"
548
+ onClick={() => window.open('https://studio.nebius.com/', '_blank')}
549
+ >
550
+ <div className="text-center">
551
+ <div className="text-2xl mb-1">🚀</div>
552
+ <div className="font-medium">Nebius Studio</div>
553
+ <div className="text-xs text-muted-foreground">AI model training & deployment</div>
554
+ </div>
555
+ </Button>
556
+ <Button
557
+ variant="outline"
558
+ className="h-auto p-4 hover:bg-green-50 hover:border-green-300 dark:hover:bg-green-900/20"
559
+ onClick={() => window.open('https://platform.openai.com/playground', '_blank')}
560
+ >
561
+ <div className="text-center">
562
+ <div className="text-2xl mb-1">🤖</div>
563
+ <div className="font-medium">OpenAI Playground</div>
564
+ <div className="text-xs text-muted-foreground">Test and tune prompts</div>
565
+ </div>
566
+ </Button>
567
+ <Button
568
+ variant="outline"
569
+ className="h-auto p-4 hover:bg-orange-50 hover:border-orange-300 dark:hover:bg-orange-900/20"
570
+ onClick={() => window.open('https://huggingface.co/spaces', '_blank')}
571
+ >
572
+ <div className="text-center">
573
+ <div className="text-2xl mb-1">🤗</div>
574
+ <div className="font-medium">HuggingFace</div>
575
+ <div className="text-xs text-muted-foreground">Open source AI models</div>
576
+ </div>
577
+ </Button>
578
+ </div>
579
+ </div>
580
+ </TabsContent>
581
+ </Tabs>
582
+ </CollapsibleContent>
583
+ </Collapsible>
584
+
585
+ {/* Error States */}
586
+ {(aiSearchMutation.error || documentAnalysisMutation.error || embeddingsMutation.error || queryEnhancementMutation.error) && (
587
+ <div className="mt-4">
588
+ <Card className="border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20">
589
+ <CardContent className="pt-4">
590
+ <div className="flex items-center gap-2 text-red-700 dark:text-red-300">
591
+ <AlertCircle className="w-4 h-4" />
592
+ <span className="font-medium">AI Operation Error</span>
593
+ </div>
594
+ <p className="text-sm text-red-600 dark:text-red-400 mt-1">
595
+ {(aiSearchMutation.error || documentAnalysisMutation.error || embeddingsMutation.error || queryEnhancementMutation.error)?.message}
596
+ </p>
597
+ </CardContent>
598
+ </Card>
599
+ </div>
600
+ )}
601
+ </div>
602
+ );
603
+ }
client/src/components/knowledge-base/knowledge-graph.tsx ADDED
@@ -0,0 +1,811 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
6
+ import { Slider } from "@/components/ui/slider";
7
+ import { Switch } from "@/components/ui/switch";
8
+ import { Label } from "@/components/ui/label";
9
+ import { Network, GitBranch, Zap, Eye, Play, Pause, Search } from "lucide-react";
10
+ import { Input } from "@/components/ui/input";
11
+ import ForceGraph2D from "react-force-graph-2d";
12
+ import ForceGraph3D from "react-force-graph-3d";
13
+ import * as d3 from "d3";
14
+
15
+ interface GraphNode {
16
+ id: string;
17
+ label: string;
18
+ type: "document" | "concept" | "author" | "topic" | "query";
19
+ size: number;
20
+ color: string;
21
+ metadata?: any;
22
+ x?: number;
23
+ y?: number;
24
+ z?: number;
25
+ }
26
+
27
+ interface GraphLink {
28
+ source: string | GraphNode;
29
+ target: string | GraphNode;
30
+ relationship: string;
31
+ strength: number;
32
+ color: string;
33
+ }
34
+
35
+ interface GraphData {
36
+ nodes: GraphNode[];
37
+ links: GraphLink[];
38
+ }
39
+
40
+ interface GraphStats {
41
+ totalDocuments: number;
42
+ totalConcepts: number;
43
+ totalResearchTeams: number;
44
+ totalSourceTypes: number;
45
+ }
46
+
47
+ const NODE_COLORS = {
48
+ document: "#3b82f6", // blue
49
+ concept: "#10b981", // emerald
50
+ author: "#f59e0b", // amber
51
+ topic: "#8b5cf6", // violet
52
+ query: "#ef4444", // red
53
+ };
54
+
55
+ const RELATIONSHIP_COLORS = {
56
+ "cites": "#64748b",
57
+ "authored_by": "#f59e0b",
58
+ "contains_concept": "#10b981",
59
+ "related_to": "#8b5cf6",
60
+ "searched_for": "#ef4444",
61
+ "similar_to": "#06b6d4",
62
+ };
63
+
64
+ export function KnowledgeGraph() {
65
+ const [graphData, setGraphData] = useState<GraphData>({ nodes: [], links: [] });
66
+ const [view3D, setView3D] = useState(false);
67
+ const [isAnimating, setIsAnimating] = useState(true);
68
+ const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
69
+ const [linkDistance, setLinkDistance] = useState([150]);
70
+ const [chargeStrength, setChargeStrength] = useState([-300]);
71
+ const [showLabels, setShowLabels] = useState(true);
72
+ const [isLoading, setIsLoading] = useState(true);
73
+ const [stats, setStats] = useState<GraphStats>({ totalDocuments: 0, totalConcepts: 0, totalResearchTeams: 0, totalSourceTypes: 0 });
74
+ const [searchTerm, setSearchTerm] = useState("");
75
+ const [filteredGraphData, setFilteredGraphData] = useState<GraphData>({ nodes: [], links: [] });
76
+ const [hoveredNode, setHoveredNode] = useState<string | null>(null);
77
+ const [highlightNodes, setHighlightNodes] = useState(new Set<string>());
78
+ const [highlightLinks, setHighlightLinks] = useState(new Set<string>());
79
+ const fgRef = useRef<any>();
80
+
81
+ useEffect(() => {
82
+ fetchKnowledgeGraph();
83
+ }, []);
84
+
85
+ useEffect(() => {
86
+ filterGraphData();
87
+ }, [searchTerm, graphData]);
88
+
89
+ const filterGraphData = () => {
90
+ if (!searchTerm) {
91
+ setFilteredGraphData({ nodes: [], links: [] });
92
+ return;
93
+ }
94
+
95
+ const term = searchTerm.toLowerCase();
96
+ const filteredNodes = graphData.nodes.filter(node =>
97
+ node.label.toLowerCase().includes(term) ||
98
+ node.type.toLowerCase().includes(term) ||
99
+ (node.metadata?.concept && node.metadata.concept.toLowerCase().includes(term)) ||
100
+ (node.metadata?.title && node.metadata.title.toLowerCase().includes(term))
101
+ );
102
+
103
+ const nodeIds = new Set(filteredNodes.map(n => n.id));
104
+ const filteredLinks = graphData.links.filter(link => {
105
+ const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
106
+ const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
107
+ return nodeIds.has(sourceId) && nodeIds.has(targetId);
108
+ });
109
+
110
+ setFilteredGraphData({ nodes: filteredNodes, links: filteredLinks });
111
+ };
112
+
113
+ const fetchKnowledgeGraph = async () => {
114
+ try {
115
+ setIsLoading(true);
116
+ const response = await fetch('/api/knowledge-graph');
117
+ const data = await response.json();
118
+
119
+ if (data.nodes && data.links) {
120
+ setGraphData({ nodes: data.nodes, links: data.links });
121
+ setStats(data.stats || { totalDocuments: 0, totalConcepts: 0, totalResearchTeams: 0, totalSourceTypes: 0 });
122
+ } else {
123
+ // Fallback to sample data if API fails
124
+ generateKnowledgeGraph();
125
+ }
126
+ } catch (error) {
127
+ console.error('Failed to fetch knowledge graph:', error);
128
+ // Fallback to sample data
129
+ generateKnowledgeGraph();
130
+ } finally {
131
+ setIsLoading(false);
132
+ }
133
+ };
134
+
135
+ const generateKnowledgeGraph = () => {
136
+ // Extract concepts from KnowledgeBridge sample documents
137
+ const documents = [
138
+ { id: "deepseek-r1", title: "DeepSeek-R1: Advanced Reasoning Model", concepts: ["reasoning", "thinking", "chain-of-thought"], authors: ["DeepSeek Team"], year: 2024, sourceType: "arxiv" },
139
+ { id: "nebius-ai", title: "Nebius AI Platform", concepts: ["cloud-ai", "scaling", "embeddings"], authors: ["Nebius Team"], year: 2024, sourceType: "github" },
140
+ { id: "semantic-search", title: "Semantic Search with BGE Models", concepts: ["embeddings", "similarity", "retrieval"], authors: ["BAAI Team"], year: 2024, sourceType: "arxiv" },
141
+ { id: "modal-compute", title: "Modal Distributed Computing", concepts: ["serverless", "scaling", "distributed"], authors: ["Modal Team"], year: 2024, sourceType: "github" },
142
+ { id: "url-validation", title: "Smart URL Validation Systems", concepts: ["validation", "filtering", "content-verification"], authors: ["Web Research Group"], year: 2024, sourceType: "web" },
143
+ { id: "multi-source", title: "Multi-Source Information Retrieval", concepts: ["search", "aggregation", "ranking"], authors: ["IR Conference"], year: 2024, sourceType: "academic" },
144
+ { id: "ai-security", title: "AI Application Security", concepts: ["rate-limiting", "validation", "middleware"], authors: ["Security Researchers"], year: 2024, sourceType: "github" },
145
+ { id: "react-query", title: "TanStack Query for Data Fetching", concepts: ["caching", "state-management", "performance"], authors: ["TanStack Team"], year: 2024, sourceType: "github" },
146
+ { id: "typescript-safety", title: "TypeScript for Type Safety", concepts: ["type-safety", "compilation", "validation"], authors: ["TypeScript Team"], year: 2024, sourceType: "web" },
147
+ { id: "knowledge-graphs", title: "Interactive Knowledge Graphs", concepts: ["visualization", "relationships", "d3js"], authors: ["DataViz Community"], year: 2024, sourceType: "github" },
148
+ { id: "github-api", title: "GitHub API for Repository Search", concepts: ["api-integration", "search", "filtering"], authors: ["GitHub Team"], year: 2024, sourceType: "web" },
149
+ { id: "arxiv-papers", title: "ArXiv Academic Paper Search", concepts: ["academic-search", "paper-validation", "research"], authors: ["arXiv Team"], year: 2024, sourceType: "academic" },
150
+ ];
151
+
152
+ const nodes: GraphNode[] = [];
153
+ const links: GraphLink[] = [];
154
+
155
+ // Add document nodes
156
+ documents.forEach(doc => {
157
+ nodes.push({
158
+ id: doc.id,
159
+ label: doc.title,
160
+ type: "document",
161
+ size: 12,
162
+ color: NODE_COLORS.document,
163
+ metadata: doc
164
+ });
165
+ });
166
+
167
+ // Extract unique concepts and create concept nodes
168
+ const allConcepts = new Set<string>();
169
+ documents.forEach(doc => doc.concepts.forEach(concept => allConcepts.add(concept)));
170
+
171
+ allConcepts.forEach(concept => {
172
+ const relatedDocs = documents.filter(doc => doc.concepts.includes(concept));
173
+ nodes.push({
174
+ id: `concept_${concept}`,
175
+ label: concept,
176
+ type: "concept",
177
+ size: 8 + relatedDocs.length * 2,
178
+ color: NODE_COLORS.concept,
179
+ metadata: { relatedDocuments: relatedDocs.length }
180
+ });
181
+
182
+ // Link concepts to documents
183
+ relatedDocs.forEach(doc => {
184
+ links.push({
185
+ source: doc.id,
186
+ target: `concept_${concept}`,
187
+ relationship: "contains_concept",
188
+ strength: 1,
189
+ color: RELATIONSHIP_COLORS.contains_concept
190
+ });
191
+ });
192
+ });
193
+
194
+ // Extract unique authors and create author nodes
195
+ const allAuthors = new Set<string>();
196
+ documents.forEach(doc => doc.authors.forEach(author => allAuthors.add(author)));
197
+
198
+ allAuthors.forEach(author => {
199
+ const authoredDocs = documents.filter(doc => doc.authors.includes(author));
200
+ nodes.push({
201
+ id: `author_${author}`,
202
+ label: author,
203
+ type: "author",
204
+ size: 6 + authoredDocs.length,
205
+ color: NODE_COLORS.author,
206
+ metadata: { publications: authoredDocs.length }
207
+ });
208
+
209
+ // Link authors to documents
210
+ authoredDocs.forEach(doc => {
211
+ links.push({
212
+ source: `author_${author}`,
213
+ target: doc.id,
214
+ relationship: "authored_by",
215
+ strength: 0.8,
216
+ color: RELATIONSHIP_COLORS.authored_by
217
+ });
218
+ });
219
+ });
220
+
221
+ // Create topic clusters for KnowledgeBridge features
222
+ const topics = [
223
+ { id: "ai_models", name: "AI Models & Processing", docs: ["deepseek-r1", "nebius-ai", "semantic-search"] },
224
+ { id: "infrastructure", name: "Infrastructure & Scaling", docs: ["modal-compute", "ai-security", "react-query"] },
225
+ { id: "search_systems", name: "Search & Retrieval", docs: ["multi-source", "url-validation", "github-api"] },
226
+ { id: "data_visualization", name: "Data & Visualization", docs: ["knowledge-graphs", "typescript-safety"] },
227
+ { id: "academic_integration", name: "Academic Integration", docs: ["arxiv-papers", "semantic-search"] },
228
+ { id: "web_technologies", name: "Web Technologies", docs: ["react-query", "typescript-safety", "ai-security"] }
229
+ ];
230
+
231
+ topics.forEach(topic => {
232
+ nodes.push({
233
+ id: topic.id,
234
+ label: topic.name,
235
+ type: "topic",
236
+ size: 10,
237
+ color: NODE_COLORS.topic,
238
+ metadata: { documentCount: topic.docs.length }
239
+ });
240
+
241
+ // Link topics to documents
242
+ topic.docs.forEach(docId => {
243
+ if (documents.find(d => d.id === docId)) {
244
+ links.push({
245
+ source: topic.id,
246
+ target: docId,
247
+ relationship: "related_to",
248
+ strength: 0.6,
249
+ color: RELATIONSHIP_COLORS.related_to
250
+ });
251
+ }
252
+ });
253
+ });
254
+
255
+ // Add similarity links between documents with shared concepts
256
+ documents.forEach(doc1 => {
257
+ documents.forEach(doc2 => {
258
+ if (doc1.id !== doc2.id) {
259
+ const sharedConcepts = doc1.concepts.filter(c => doc2.concepts.includes(c));
260
+ if (sharedConcepts.length >= 2) {
261
+ links.push({
262
+ source: doc1.id,
263
+ target: doc2.id,
264
+ relationship: "similar_to",
265
+ strength: sharedConcepts.length * 0.3,
266
+ color: RELATIONSHIP_COLORS.similar_to
267
+ });
268
+ }
269
+ }
270
+ });
271
+ });
272
+
273
+ // Add sample search queries for KnowledgeBridge
274
+ const sampleQueries = [
275
+ { id: "query_semantic", text: "semantic search with embeddings", relatedDocs: ["semantic-search", "nebius-ai"] },
276
+ { id: "query_security", text: "AI application security middleware", relatedDocs: ["ai-security", "url-validation"] },
277
+ { id: "query_distributed", text: "distributed AI processing", relatedDocs: ["modal-compute", "nebius-ai"] }
278
+ ];
279
+
280
+ sampleQueries.forEach(query => {
281
+ nodes.push({
282
+ id: query.id,
283
+ label: query.text,
284
+ type: "query",
285
+ size: 6,
286
+ color: NODE_COLORS.query,
287
+ metadata: { searchText: query.text }
288
+ });
289
+
290
+ query.relatedDocs.forEach(docId => {
291
+ links.push({
292
+ source: query.id,
293
+ target: docId,
294
+ relationship: "searched_for",
295
+ strength: 0.4,
296
+ color: RELATIONSHIP_COLORS.searched_for
297
+ });
298
+ });
299
+ });
300
+
301
+ setGraphData({ nodes, links });
302
+ };
303
+
304
+ const handleNodeClick = (node: any) => {
305
+ setSelectedNode(node);
306
+ if (fgRef.current) {
307
+ // Center the camera on the clicked node
308
+ if (view3D) {
309
+ fgRef.current.cameraPosition(
310
+ { x: node.x, y: node.y, z: node.z + 100 },
311
+ node,
312
+ 3000
313
+ );
314
+ } else {
315
+ fgRef.current.centerAt(node.x, node.y, 1000);
316
+ fgRef.current.zoom(2, 1000);
317
+ }
318
+ }
319
+ };
320
+
321
+ const updateHighlight = () => {
322
+ const currentData = filteredGraphData.nodes.length > 0 ? filteredGraphData : graphData;
323
+
324
+ setHighlightNodes(highlightNodes);
325
+ setHighlightLinks(highlightLinks);
326
+ };
327
+
328
+ const handleNodeHover = (node: any) => {
329
+ if (!node) {
330
+ setHoveredNode(null);
331
+ setHighlightNodes(new Set());
332
+ setHighlightLinks(new Set());
333
+ return;
334
+ }
335
+
336
+ setHoveredNode(node.id);
337
+
338
+ const newHighlightNodes = new Set<string>();
339
+ const newHighlightLinks = new Set<string>();
340
+
341
+ newHighlightNodes.add(node.id);
342
+
343
+ const currentData = filteredGraphData.nodes.length > 0 ? filteredGraphData : graphData;
344
+
345
+ currentData.links.forEach(link => {
346
+ const sourceId = typeof link.source === 'string' ? link.source : link.source.id;
347
+ const targetId = typeof link.target === 'string' ? link.target : link.target.id;
348
+
349
+ if (sourceId === node.id || targetId === node.id) {
350
+ newHighlightLinks.add(`${sourceId}-${targetId}`);
351
+ newHighlightNodes.add(sourceId);
352
+ newHighlightNodes.add(targetId);
353
+ }
354
+ });
355
+
356
+ setHighlightNodes(newHighlightNodes);
357
+ setHighlightLinks(newHighlightLinks);
358
+ };
359
+
360
+ const resetView = () => {
361
+ if (fgRef.current) {
362
+ if (view3D) {
363
+ fgRef.current.cameraPosition({ x: 0, y: 0, z: 300 }, { x: 0, y: 0, z: 0 }, 2000);
364
+ } else {
365
+ fgRef.current.zoomToFit(2000);
366
+ }
367
+ }
368
+ setSelectedNode(null);
369
+ };
370
+
371
+ const toggleAnimation = () => {
372
+ setIsAnimating(!isAnimating);
373
+ if (fgRef.current) {
374
+ if (isAnimating) {
375
+ fgRef.current.pauseAnimation();
376
+ } else {
377
+ fgRef.current.resumeAnimation();
378
+ }
379
+ }
380
+ };
381
+
382
+ const getNodeLabel = (node: any) => {
383
+ if (!showLabels) return "";
384
+ return node.label.length > 20 ? node.label.substring(0, 20) + "..." : node.label;
385
+ };
386
+
387
+ const getNodeSize = (node: any) => {
388
+ return node.size || 8;
389
+ };
390
+
391
+ const getNodeColor = (node: any) => {
392
+ if (hoveredNode === node.id) return "#ff6b6b"; // Red for hovered node
393
+ if (highlightNodes.has(node.id)) return "#4ecdc4"; // Teal for connected nodes
394
+ return node.color; // Original color
395
+ };
396
+
397
+ const getLinkWidth = (link: any) => {
398
+ const baseWidth = Math.max(0.5, link.strength * 3);
399
+ const linkId = `${link.source}-${link.target}`;
400
+ return highlightLinks.has(linkId) ? baseWidth * 2 : baseWidth;
401
+ };
402
+
403
+ const getLinkColor = (link: any) => {
404
+ const linkId = `${link.source}-${link.target}`;
405
+ return highlightLinks.has(linkId) ? "#ff6b6b" : link.color;
406
+ };
407
+
408
+ return (
409
+ <div className="space-y-6">
410
+ {/* Header */}
411
+ <div className="flex items-center justify-between">
412
+ <div>
413
+ <h2 className="text-2xl font-bold flex items-center gap-2">
414
+ <Network className="h-6 w-6" />
415
+ KnowledgeBridge Knowledge Graph
416
+ </h2>
417
+ <p className="text-muted-foreground">
418
+ Explore relationships between technologies, research sources, and concepts in KnowledgeBridge
419
+ </p>
420
+ </div>
421
+ <div className="flex items-center gap-2">
422
+ <Button variant="outline" size="sm" onClick={fetchKnowledgeGraph} disabled={isLoading}>
423
+ <GitBranch className="h-4 w-4 mr-2" />
424
+ Refresh Graph
425
+ </Button>
426
+ <Button variant="outline" size="sm" onClick={resetView}>
427
+ <Eye className="h-4 w-4 mr-2" />
428
+ Reset View
429
+ </Button>
430
+ <Button variant="outline" size="sm" onClick={toggleAnimation}>
431
+ {isAnimating ? <Pause className="h-4 w-4 mr-2" /> : <Play className="h-4 w-4 mr-2" />}
432
+ {isAnimating ? "Pause" : "Resume"}
433
+ </Button>
434
+ </div>
435
+ </div>
436
+
437
+ <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
438
+ {/* Controls Panel */}
439
+ <Card className="lg:col-span-1">
440
+ <CardHeader>
441
+ <CardTitle className="text-lg">Graph Controls</CardTitle>
442
+ </CardHeader>
443
+ <CardContent className="space-y-4">
444
+ {/* Search Filter */}
445
+ <div className="space-y-2">
446
+ <Label className="text-sm font-medium">Search Graph</Label>
447
+ <div className="relative">
448
+ <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
449
+ <Input
450
+ placeholder="Filter nodes..."
451
+ value={searchTerm}
452
+ onChange={(e) => setSearchTerm(e.target.value)}
453
+ className="pl-8"
454
+ />
455
+ </div>
456
+ {filteredGraphData.nodes.length > 0 && (
457
+ <div className="text-xs text-muted-foreground">
458
+ Showing {filteredGraphData.nodes.length} of {graphData.nodes.length} nodes
459
+ </div>
460
+ )}
461
+ </div>
462
+
463
+ {/* View Toggle */}
464
+ <div className="flex items-center justify-between">
465
+ <Label htmlFor="view-toggle" className="text-sm font-medium">
466
+ 3D View
467
+ </Label>
468
+ <Switch
469
+ id="view-toggle"
470
+ checked={view3D}
471
+ onCheckedChange={setView3D}
472
+ />
473
+ </div>
474
+
475
+ {/* Show Labels Toggle */}
476
+ <div className="flex items-center justify-between">
477
+ <Label htmlFor="labels-toggle" className="text-sm font-medium">
478
+ Show Labels
479
+ </Label>
480
+ <Switch
481
+ id="labels-toggle"
482
+ checked={showLabels}
483
+ onCheckedChange={setShowLabels}
484
+ />
485
+ </div>
486
+
487
+ {/* Link Distance */}
488
+ <div className="space-y-2">
489
+ <Label className="text-sm font-medium">Link Distance</Label>
490
+ <Slider
491
+ value={linkDistance}
492
+ onValueChange={setLinkDistance}
493
+ max={300}
494
+ min={50}
495
+ step={10}
496
+ className="w-full"
497
+ />
498
+ <div className="text-xs text-muted-foreground text-center">
499
+ {linkDistance[0]}px
500
+ </div>
501
+ </div>
502
+
503
+ {/* Charge Strength */}
504
+ <div className="space-y-2">
505
+ <Label className="text-sm font-medium">Node Repulsion</Label>
506
+ <Slider
507
+ value={chargeStrength}
508
+ onValueChange={setChargeStrength}
509
+ max={-50}
510
+ min={-500}
511
+ step={10}
512
+ className="w-full"
513
+ />
514
+ <div className="text-xs text-muted-foreground text-center">
515
+ {chargeStrength[0]}
516
+ </div>
517
+ </div>
518
+
519
+ {/* Legend */}
520
+ <div className="space-y-2">
521
+ <Label className="text-sm font-medium">Node Types</Label>
522
+ <div className="grid grid-cols-2 gap-1 text-xs">
523
+ {Object.entries(NODE_COLORS).map(([type, color]) => (
524
+ <div key={type} className="flex items-center gap-1">
525
+ <div
526
+ className="w-3 h-3 rounded-full"
527
+ style={{ backgroundColor: color }}
528
+ />
529
+ <span className="capitalize">{type}</span>
530
+ </div>
531
+ ))}
532
+ </div>
533
+ </div>
534
+
535
+ {/* Graph Stats */}
536
+ <div className="pt-4 border-t space-y-1 text-sm">
537
+ <div className="flex justify-between">
538
+ <span>Nodes:</span>
539
+ <span className="font-medium">{graphData.nodes.length}</span>
540
+ </div>
541
+ <div className="flex justify-between">
542
+ <span>Links:</span>
543
+ <span className="font-medium">{graphData.links.length}</span>
544
+ </div>
545
+ </div>
546
+ </CardContent>
547
+ </Card>
548
+
549
+ {/* Graph Visualization */}
550
+ <Card className="lg:col-span-2">
551
+ <CardContent className="p-0">
552
+ <div className="h-[600px] w-full bg-background rounded-lg overflow-hidden relative flex items-center justify-center">
553
+ {isLoading && (
554
+ <div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
555
+ <div className="text-center">
556
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div>
557
+ <p className="text-sm text-muted-foreground">Building knowledge graph...</p>
558
+ </div>
559
+ </div>
560
+ )}
561
+ {view3D ? (
562
+ <ForceGraph3D
563
+ ref={fgRef}
564
+ graphData={filteredGraphData.nodes.length > 0 ? filteredGraphData : graphData}
565
+ nodeLabel={getNodeLabel}
566
+ nodeColor={getNodeColor}
567
+ nodeVal={getNodeSize}
568
+ linkColor={getLinkColor}
569
+ linkWidth={getLinkWidth}
570
+ linkDirectionalParticles={2}
571
+ linkDirectionalParticleSpeed={0.01}
572
+ onNodeClick={handleNodeClick}
573
+ onNodeHover={handleNodeHover}
574
+ d3AlphaDecay={0.01}
575
+ d3VelocityDecay={0.1}
576
+ enableNodeDrag={true}
577
+ enableNavigationControls={true}
578
+ controlType="orbit"
579
+ backgroundColor="rgba(0,0,0,0)"
580
+ onEngineStop={() => {
581
+ if (fgRef.current) {
582
+ // Center the 3D graph with proper camera positioning
583
+ setTimeout(() => {
584
+ const distance = Math.max(400, graphData.nodes.length * 20);
585
+ fgRef.current.cameraPosition({ x: 0, y: 0, z: distance }, { x: 0, y: 0, z: 0 }, 1500);
586
+ }, 100);
587
+ }
588
+ }}
589
+ />
590
+ ) : (
591
+ <ForceGraph2D
592
+ ref={fgRef}
593
+ graphData={filteredGraphData.nodes.length > 0 ? filteredGraphData : graphData}
594
+ nodeLabel={getNodeLabel}
595
+ nodeColor={getNodeColor}
596
+ nodeVal={getNodeSize}
597
+ linkColor={getLinkColor}
598
+ linkWidth={getLinkWidth}
599
+ linkDirectionalParticles={1}
600
+ linkDirectionalParticleSpeed={0.005}
601
+ onNodeClick={handleNodeClick}
602
+ onNodeHover={handleNodeHover}
603
+ d3AlphaDecay={0.01}
604
+ d3VelocityDecay={0.1}
605
+ enableNodeDrag={true}
606
+ enableZoomInteraction={true}
607
+ backgroundColor="rgba(0,0,0,0)"
608
+ linkDirectionalArrowLength={3}
609
+ linkDirectionalArrowRelPos={1}
610
+ onEngineStop={() => {
611
+ if (fgRef.current) {
612
+ // Center the graph with proper zoom
613
+ setTimeout(() => {
614
+ fgRef.current.zoomToFit(400, 80);
615
+ }, 100);
616
+ }
617
+ }}
618
+ />
619
+ )}
620
+ </div>
621
+ </CardContent>
622
+ </Card>
623
+
624
+ {/* Node Details Panel */}
625
+ <Card className="lg:col-span-1">
626
+ <CardHeader className="pb-3">
627
+ <CardTitle className="text-lg">Node Details</CardTitle>
628
+ <CardDescription>
629
+ Click on a node to view details
630
+ </CardDescription>
631
+ </CardHeader>
632
+ <CardContent className="pt-0">
633
+ {selectedNode ? (
634
+ <div className="space-y-4">
635
+ <div>
636
+ <Badge variant="outline" className="mb-2">
637
+ {selectedNode.type}
638
+ </Badge>
639
+ <h3 className="font-semibold text-lg">{selectedNode.label}</h3>
640
+ </div>
641
+
642
+ {selectedNode.metadata && (
643
+ <div className="space-y-2 text-sm">
644
+ {selectedNode.type === "document" && (
645
+ <>
646
+ {selectedNode.metadata.year && (
647
+ <div>
648
+ <span className="font-medium">Year:</span> {selectedNode.metadata.year}
649
+ </div>
650
+ )}
651
+ {selectedNode.metadata.authors && Array.isArray(selectedNode.metadata.authors) && (
652
+ <div>
653
+ <span className="font-medium">Authors:</span>
654
+ <div className="flex flex-wrap gap-1 mt-1">
655
+ {selectedNode.metadata.authors.map((author: string) => (
656
+ <Badge key={author} variant="secondary" className="text-xs">
657
+ {author}
658
+ </Badge>
659
+ ))}
660
+ </div>
661
+ </div>
662
+ )}
663
+ {selectedNode.metadata.venue && (
664
+ <div>
665
+ <span className="font-medium">Venue:</span> {selectedNode.metadata.venue}
666
+ </div>
667
+ )}
668
+ {selectedNode.metadata.sourceType && (
669
+ <div>
670
+ <span className="font-medium">Source Type:</span> {selectedNode.metadata.sourceType}
671
+ </div>
672
+ )}
673
+ {selectedNode.metadata.title && selectedNode.metadata.title !== selectedNode.label && (
674
+ <div>
675
+ <span className="font-medium">Full Title:</span>
676
+ <div className="text-xs text-muted-foreground mt-1">
677
+ {selectedNode.metadata.title}
678
+ </div>
679
+ </div>
680
+ )}
681
+ </>
682
+ )}
683
+
684
+ {selectedNode.type === "concept" && (
685
+ <div>
686
+ <span className="font-medium">Related Documents:</span> {selectedNode.metadata.relatedDocuments}
687
+ </div>
688
+ )}
689
+
690
+ {selectedNode.type === "author" && (
691
+ <>
692
+ {selectedNode.metadata.teamName && (
693
+ <div>
694
+ <span className="font-medium">Team:</span> {selectedNode.metadata.teamName}
695
+ </div>
696
+ )}
697
+ {selectedNode.metadata.publicationCount && (
698
+ <div>
699
+ <span className="font-medium">Publications:</span> {selectedNode.metadata.publicationCount}
700
+ </div>
701
+ )}
702
+ {selectedNode.metadata.publications && (
703
+ <div>
704
+ <span className="font-medium">Publications:</span> {selectedNode.metadata.publications}
705
+ </div>
706
+ )}
707
+ </>
708
+ )}
709
+
710
+ {selectedNode.type === "topic" && (
711
+ <div>
712
+ <span className="font-medium">Documents:</span> {selectedNode.metadata.documentCount}
713
+ </div>
714
+ )}
715
+
716
+ {selectedNode.type === "query" && (
717
+ <div>
718
+ <span className="font-medium">Search Text:</span> {selectedNode.metadata.searchText}
719
+ </div>
720
+ )}
721
+ </div>
722
+ )}
723
+
724
+ {/* Connected Nodes */}
725
+ <div>
726
+ <h4 className="font-medium mb-2">Connected Nodes</h4>
727
+ <div className="space-y-1 max-h-32 overflow-y-auto">
728
+ {graphData.links
729
+ .filter(link => {
730
+ // Handle both string and object references for source/target
731
+ const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
732
+ const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
733
+ return sourceId === selectedNode.id || targetId === selectedNode.id;
734
+ })
735
+ .map((link, idx) => {
736
+ const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
737
+ const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
738
+ const connectedNodeId = sourceId === selectedNode.id ? targetId : sourceId;
739
+ const connectedNode = graphData.nodes.find(n => n.id === connectedNodeId);
740
+ return connectedNode ? (
741
+ <div key={idx} className="text-xs p-2 bg-muted rounded cursor-pointer hover:bg-muted/80"
742
+ onClick={() => setSelectedNode(connectedNode)}>
743
+ <div className="font-medium">{connectedNode.label}</div>
744
+ <div className="text-muted-foreground">
745
+ {link.relationship?.replace(/_/g, ' ') || 'related'}
746
+ </div>
747
+ </div>
748
+ ) : null;
749
+ })}
750
+ {graphData.links.filter(link => {
751
+ const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
752
+ const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
753
+ return sourceId === selectedNode.id || targetId === selectedNode.id;
754
+ }).length === 0 && (
755
+ <div className="text-xs text-muted-foreground p-2">
756
+ No connected nodes found
757
+ </div>
758
+ )}
759
+ </div>
760
+ </div>
761
+ </div>
762
+ ) : (
763
+ <div className="text-center text-muted-foreground py-8">
764
+ <GitBranch className="h-12 w-12 mx-auto mb-4 opacity-50" />
765
+ <p>Select a node to view its details and connections</p>
766
+ </div>
767
+ )}
768
+ </CardContent>
769
+ </Card>
770
+ </div>
771
+
772
+ {/* Insights Panel */}
773
+ <Card>
774
+ <CardHeader>
775
+ <CardTitle className="flex items-center gap-2">
776
+ <Zap className="h-5 w-5" />
777
+ Graph Insights
778
+ </CardTitle>
779
+ </CardHeader>
780
+ <CardContent>
781
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
782
+ <div className="text-center">
783
+ <div className="text-2xl font-bold text-blue-600">
784
+ {stats.totalDocuments || graphData.nodes.filter(n => n.type === "document").length}
785
+ </div>
786
+ <div className="text-sm text-muted-foreground">Knowledge Sources</div>
787
+ </div>
788
+ <div className="text-center">
789
+ <div className="text-2xl font-bold text-emerald-600">
790
+ {stats.totalConcepts || graphData.nodes.filter(n => n.type === "concept").length}
791
+ </div>
792
+ <div className="text-sm text-muted-foreground">Technical Concepts</div>
793
+ </div>
794
+ <div className="text-center">
795
+ <div className="text-2xl font-bold text-amber-600">
796
+ {stats.totalResearchTeams || graphData.nodes.filter(n => n.type === "author").length}
797
+ </div>
798
+ <div className="text-sm text-muted-foreground">Technology Teams</div>
799
+ </div>
800
+ <div className="text-center">
801
+ <div className="text-2xl font-bold text-violet-600">
802
+ {stats.totalSourceTypes || graphData.nodes.filter(n => n.type === "topic").length}
803
+ </div>
804
+ <div className="text-sm text-muted-foreground">Technology Areas</div>
805
+ </div>
806
+ </div>
807
+ </CardContent>
808
+ </Card>
809
+ </div>
810
+ );
811
+ }
client/src/components/knowledge-base/result-card.tsx ADDED
@@ -0,0 +1,509 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { ChevronDown, ChevronUp, ExternalLink, Quote, Bookmark, FileText, Globe, Github, GraduationCap, Brain, Copy, Check, Volume2, VolumeX } from "lucide-react";
5
+ import { type DocumentWithContext } from "@shared/schema";
6
+
7
+ // Helper function to safely render metadata
8
+ const renderMetadataValue = (value: unknown): string => {
9
+ if (Array.isArray(value)) {
10
+ return value.map(String).join(", ");
11
+ }
12
+ return String(value);
13
+ };
14
+
15
+ interface ResultCardProps {
16
+ document: DocumentWithContext;
17
+ isExpanded: boolean;
18
+ isSaved?: boolean;
19
+ onToggleExpanded: () => void;
20
+ onAddCitation: (documentId: number, citationText: string, section?: string, pageNumber?: number) => void;
21
+ onSaveDocument?: (documentId: number) => void;
22
+ }
23
+
24
+ const sourceTypeIcons = {
25
+ pdf: FileText,
26
+ web: Globe,
27
+ code: Github,
28
+ academic: GraduationCap,
29
+ };
30
+
31
+ const sourceTypeColors = {
32
+ pdf: "text-red-500",
33
+ web: "text-blue-500",
34
+ code: "text-slate-700",
35
+ academic: "text-purple-500",
36
+ };
37
+
38
+ export default function ResultCard({
39
+ document,
40
+ isExpanded,
41
+ isSaved = false,
42
+ onToggleExpanded,
43
+ onAddCitation,
44
+ onSaveDocument
45
+ }: ResultCardProps) {
46
+ const [isAddingCitation, setIsAddingCitation] = useState(false);
47
+ const [isExplaining, setIsExplaining] = useState(false);
48
+ const [explanation, setExplanation] = useState("");
49
+ const [copiedFormat, setCopiedFormat] = useState<string | null>(null);
50
+ const [isPlayingAudio, setIsPlayingAudio] = useState(false);
51
+ const [selectedVoice, setSelectedVoice] = useState<SpeechSynthesisVoice | null>(null);
52
+
53
+ const IconComponent = sourceTypeIcons[document.sourceType as keyof typeof sourceTypeIcons] || FileText;
54
+ const iconColor = sourceTypeColors[document.sourceType as keyof typeof sourceTypeColors] || "text-gray-500";
55
+
56
+ useEffect(() => {
57
+ const loadVoices = () => {
58
+ if ('speechSynthesis' in window) {
59
+ const voices = window.speechSynthesis.getVoices();
60
+ if (voices.length > 0 && !selectedVoice) {
61
+ // Prefer calm, soothing voices
62
+ const preferredVoice = voices.find(voice =>
63
+ voice.name.includes('Samantha') ||
64
+ voice.name.includes('Victoria') ||
65
+ voice.name.includes('Google UK English Female') ||
66
+ voice.name.includes('Microsoft Zira') ||
67
+ voice.name.includes('Karen') ||
68
+ voice.name.includes('Fiona') ||
69
+ voice.name.includes('Serena')
70
+ ) || voices.find(voice => voice.lang.startsWith('en') && voice.name.includes('Female')) || voices.find(voice => voice.lang.startsWith('en')) || voices[0];
71
+
72
+ setSelectedVoice(preferredVoice);
73
+ }
74
+ }
75
+ };
76
+
77
+ loadVoices();
78
+ if ('speechSynthesis' in window) {
79
+ window.speechSynthesis.onvoiceschanged = loadVoices;
80
+ }
81
+ }, [selectedVoice]);
82
+
83
+ const getRelevanceColor = (score: number) => {
84
+ if (score >= 0.9) return "bg-emerald-100 text-emerald-700";
85
+ if (score >= 0.8) return "bg-blue-100 text-blue-700";
86
+ if (score >= 0.7) return "bg-amber-100 text-amber-700";
87
+ return "bg-slate-100 text-slate-700";
88
+ };
89
+
90
+ const getSourceTypeLabel = (type: string) => {
91
+ const labels = {
92
+ pdf: "PDF Document",
93
+ web: "Web Page",
94
+ code: "GitHub Repository",
95
+ academic: "Academic Paper",
96
+ };
97
+ return labels[type as keyof typeof labels] || "Document";
98
+ };
99
+
100
+ const handleAddCitation = async () => {
101
+ setIsAddingCitation(true);
102
+ try {
103
+ await onAddCitation(
104
+ document.id,
105
+ document.snippet,
106
+ "Main Content",
107
+ (document.metadata && typeof document.metadata === 'object' && 'pageNumber' in document.metadata ? Number(document.metadata.pageNumber) || undefined : undefined)
108
+ );
109
+ } finally {
110
+ setIsAddingCitation(false);
111
+ }
112
+ };
113
+
114
+ const handleViewSource = () => {
115
+ if (document.url) {
116
+ window.open(document.url, '_blank', 'noopener,noreferrer');
117
+ }
118
+ };
119
+
120
+ const handleSaveDocument = () => {
121
+ if (onSaveDocument) {
122
+ onSaveDocument(document.id);
123
+ }
124
+ };
125
+
126
+ const highlightSearchHits = (text: string, query: string) => {
127
+ if (!query.trim()) return text;
128
+
129
+ const words = query.toLowerCase().split(/\s+/).filter(word => word.length > 2);
130
+ let highlightedText = text;
131
+
132
+ words.forEach(word => {
133
+ const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
134
+ const regex = new RegExp(`(${escapedWord})`, 'gi');
135
+ highlightedText = highlightedText.replace(regex, '<mark class="bg-yellow-200 dark:bg-yellow-600 px-1 rounded">$1</mark>');
136
+ });
137
+
138
+ return highlightedText;
139
+ };
140
+
141
+ const handleExplain = async () => {
142
+ setIsExplaining(true);
143
+ try {
144
+ const response = await fetch('/api/explain', {
145
+ method: 'POST',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify({
148
+ title: document.title,
149
+ snippet: document.snippet,
150
+ content: document.content.substring(0, 1000)
151
+ })
152
+ });
153
+
154
+ if (response.ok) {
155
+ const data = await response.json();
156
+ setExplanation(data.explanation);
157
+
158
+ // Automatically play audio explanation
159
+ playExplanationAudio(data.explanation);
160
+ } else {
161
+ setExplanation("Unable to generate explanation at this time.");
162
+ }
163
+ } catch (error) {
164
+ setExplanation("Error generating explanation.");
165
+ } finally {
166
+ setIsExplaining(false);
167
+ }
168
+ };
169
+
170
+ const playExplanationAudio = (text: string) => {
171
+ if ('speechSynthesis' in window) {
172
+ // Stop any current speech
173
+ window.speechSynthesis.cancel();
174
+
175
+ // Add friendly, engaging intro phrase
176
+ const engagingText = `Here's what I discovered: ${text}. Quite fascinating stuff!`;
177
+ const utterance = new SpeechSynthesisUtterance(engagingText);
178
+
179
+ // Use selected voice or get a more engaging default
180
+ if (selectedVoice) {
181
+ utterance.voice = selectedVoice;
182
+ } else {
183
+ const voices = window.speechSynthesis.getVoices();
184
+ const preferredVoice = voices.find(voice =>
185
+ voice.name.includes('Samantha') ||
186
+ voice.name.includes('Victoria') ||
187
+ voice.name.includes('Google UK English Female') ||
188
+ voice.name.includes('Microsoft Zira') ||
189
+ voice.name.includes('Karen') ||
190
+ voice.name.includes('Fiona') ||
191
+ voice.name.includes('Serena')
192
+ ) || voices.find(voice => voice.lang.startsWith('en') && voice.name.includes('Female')) || voices.find(voice => voice.lang.startsWith('en')) || voices[0];
193
+
194
+ if (preferredVoice) {
195
+ utterance.voice = preferredVoice;
196
+ }
197
+ }
198
+
199
+ // Engaging yet pleasant voice settings
200
+ utterance.rate = 1.05; // Slightly faster, more engaging
201
+ utterance.pitch = 1.1; // Warm, friendly pitch
202
+ utterance.volume = 0.9; // Clear but not overwhelming
203
+
204
+ utterance.onstart = () => setIsPlayingAudio(true);
205
+ utterance.onend = () => setIsPlayingAudio(false);
206
+ utterance.onerror = () => setIsPlayingAudio(false);
207
+
208
+ window.speechSynthesis.speak(utterance);
209
+ }
210
+ };
211
+
212
+ const toggleAudio = () => {
213
+ if (isPlayingAudio) {
214
+ window.speechSynthesis.cancel();
215
+ setIsPlayingAudio(false);
216
+ } else if (explanation) {
217
+ playExplanationAudio(explanation);
218
+ }
219
+ };
220
+
221
+ const copyToClipboard = async (format: 'markdown' | 'bibtex') => {
222
+ let text = '';
223
+
224
+ if (format === 'markdown') {
225
+ text = `[${document.title}](${document.url || '#'}) - ${document.source}`;
226
+ } else if (format === 'bibtex') {
227
+ const year = (document.metadata && typeof document.metadata === 'object' && 'year' in document.metadata ? Number(document.metadata.year) || new Date().getFullYear() : new Date().getFullYear());
228
+ const authors = (document.metadata && typeof document.metadata === 'object' && 'authors' in document.metadata ? (Array.isArray(document.metadata.authors) ? document.metadata.authors as string[] : [String(document.metadata.authors)]) : ['Unknown']);
229
+ text = `@article{doc${document.id},
230
+ title={${document.title}},
231
+ author={${Array.isArray(authors) ? authors.join(' and ') : authors}},
232
+ year={${year}},
233
+ url={${document.url || ''}},
234
+ note={${document.source}}
235
+ }`;
236
+ }
237
+
238
+ try {
239
+ await navigator.clipboard.writeText(text);
240
+ setCopiedFormat(format);
241
+ setTimeout(() => setCopiedFormat(null), 2000);
242
+ } catch (error) {
243
+ console.error('Failed to copy:', error);
244
+ }
245
+ };
246
+
247
+ const getTrustBadge = () => {
248
+ const sourceType = document.sourceType;
249
+ const source = document.source.toLowerCase();
250
+
251
+ if (sourceType === 'academic' || source.includes('arxiv') || source.includes('acm') || source.includes('ieee')) {
252
+ return { icon: '🔵', label: 'Peer-reviewed', color: 'text-blue-600 bg-blue-50' };
253
+ } else if (sourceType === 'web' && (source.includes('docs.') || source.includes('official') || source.includes('.org'))) {
254
+ return { icon: '🟢', label: 'Official docs', color: 'text-green-600 bg-green-50' };
255
+ } else {
256
+ return { icon: '⚪', label: 'Web source', color: 'text-gray-600 bg-gray-50' };
257
+ }
258
+ };
259
+
260
+ const trustBadge = getTrustBadge();
261
+
262
+ return (
263
+ <div className="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden transition-all duration-300 hover:shadow-md dark:hover:shadow-lg">
264
+ <div className="p-6">
265
+ <div className="flex items-start justify-between mb-4">
266
+ <div className="flex-1">
267
+ <div className="flex items-center gap-3 mb-2">
268
+ <div className="flex items-center gap-2">
269
+ <IconComponent className={`w-4 h-4 ${iconColor}`} />
270
+ <span className="text-sm font-medium text-slate-600">
271
+ {getSourceTypeLabel(document.sourceType)}
272
+ </span>
273
+ </div>
274
+ <Badge
275
+ variant="secondary"
276
+ className={`text-xs font-medium ${getRelevanceColor(document.relevanceScore)}`}
277
+ >
278
+ {Math.round(document.relevanceScore * 100)}% Relevance
279
+ </Badge>
280
+ <Badge
281
+ variant="outline"
282
+ className={`text-xs font-medium ${trustBadge.color} border-current`}
283
+ >
284
+ {trustBadge.icon} {trustBadge.label}
285
+ </Badge>
286
+ </div>
287
+ <h3 className="text-lg font-semibold text-slate-900 mb-2 line-clamp-2">
288
+ {document.title}
289
+ </h3>
290
+ <p className="text-sm text-slate-600 mb-3">
291
+ {document.source}
292
+ </p>
293
+ </div>
294
+ <Button
295
+ variant="ghost"
296
+ size="sm"
297
+ onClick={onToggleExpanded}
298
+ className="text-slate-400 hover:text-slate-600"
299
+ >
300
+ {isExpanded ? (
301
+ <ChevronUp className="w-4 h-4" />
302
+ ) : (
303
+ <ChevronDown className="w-4 h-4" />
304
+ )}
305
+ </Button>
306
+ </div>
307
+
308
+ {/* Snippet Preview with Highlighted Hits */}
309
+ <div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-4 mb-4">
310
+ <div
311
+ className="text-sm text-slate-700 dark:text-slate-300 leading-relaxed"
312
+ dangerouslySetInnerHTML={{
313
+ __html: highlightSearchHits(document.snippet, (document as any).searchQuery || '')
314
+ }}
315
+ />
316
+ </div>
317
+
318
+ {/* AI Explanation */}
319
+ {explanation && (
320
+ <div className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-4">
321
+ <div className="flex items-center justify-between mb-2">
322
+ <div className="flex items-center gap-2">
323
+ <Brain className="w-4 h-4 text-blue-600 dark:text-blue-400" />
324
+ <span className="text-sm font-medium text-blue-900 dark:text-blue-200">🤖 AI Assistant</span>
325
+ </div>
326
+ <Button
327
+ variant="ghost"
328
+ size="sm"
329
+ onClick={toggleAudio}
330
+ className="text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-800/50 h-8 w-8 p-0"
331
+ >
332
+ {isPlayingAudio ? (
333
+ <VolumeX className="w-4 h-4" />
334
+ ) : (
335
+ <Volume2 className="w-4 h-4" />
336
+ )}
337
+ </Button>
338
+ </div>
339
+ <div className="text-sm text-blue-800 dark:text-blue-200 leading-relaxed">
340
+ <span className="font-medium">Here's what I discovered:</span> {explanation} <span className="text-blue-600 dark:text-blue-400 font-medium">Quite fascinating stuff!</span>
341
+ </div>
342
+ {isPlayingAudio && (
343
+ <div className="mt-2 text-xs text-blue-600 dark:text-blue-400 flex items-center gap-1">
344
+ <div className="w-2 h-2 bg-blue-600 dark:bg-blue-400 rounded-full animate-pulse"></div>
345
+ <div className="w-2 h-2 bg-purple-600 dark:bg-purple-400 rounded-full animate-pulse animation-delay-100"></div>
346
+ <div className="w-2 h-2 bg-blue-600 dark:bg-blue-400 rounded-full animate-pulse animation-delay-200"></div>
347
+ Playing engaging audio explanation...
348
+ </div>
349
+ )}
350
+ </div>
351
+ )}
352
+
353
+ {/* Expanded Content */}
354
+ {isExpanded && (
355
+ <div className="animate-in slide-in-from-top-2 duration-300">
356
+ <div className="border-t border-slate-200 pt-4">
357
+ <h4 className="font-medium text-slate-900 mb-3">Additional Context</h4>
358
+ <div className="space-y-3">
359
+ {document.additionalContext?.map((context, index) => (
360
+ <div key={index} className="bg-slate-50 dark:bg-slate-900 rounded-lg p-3">
361
+ <p className="text-sm text-slate-700 dark:text-slate-300 mb-2">
362
+ <strong>{context.section}:</strong> {context.text}
363
+ </p>
364
+ {context.pageNumber && (
365
+ <span className="text-xs text-slate-500 dark:text-slate-400">
366
+ Page {context.pageNumber}
367
+ </span>
368
+ )}
369
+ </div>
370
+ )) || (
371
+ <div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-3">
372
+ <p className="text-sm text-slate-700 dark:text-slate-300 mb-2">
373
+ <strong>Full Content:</strong> {document.content.substring(0, 300)}
374
+ {document.content.length > 300 && "..."}
375
+ </p>
376
+ </div>
377
+ )}
378
+ </div>
379
+ </div>
380
+
381
+ {/* Metadata */}
382
+ {document.metadata && typeof document.metadata === 'object' ? (
383
+ <div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
384
+ <h4 className="font-medium text-slate-900 dark:text-slate-100 mb-2">Metadata</h4>
385
+ <div className="grid grid-cols-2 gap-2 text-sm">
386
+ {(() => {
387
+ const metadata = document.metadata as Record<string, unknown>;
388
+ return (
389
+ <>
390
+ {metadata.authors && (
391
+ <div>
392
+ <span className="text-slate-500 dark:text-slate-400">Authors:</span>
393
+ <span className="ml-2 text-slate-700 dark:text-slate-300">
394
+ {renderMetadataValue(metadata.authors)}
395
+ </span>
396
+ </div>
397
+ )}
398
+ {metadata.year && (
399
+ <div>
400
+ <span className="text-slate-500 dark:text-slate-400">Year:</span>
401
+ <span className="ml-2 text-slate-700 dark:text-slate-300">{renderMetadataValue(metadata.year)}</span>
402
+ </div>
403
+ )}
404
+ {metadata.citations && (
405
+ <div>
406
+ <span className="text-slate-500 dark:text-slate-400">Citations:</span>
407
+ <span className="ml-2 text-slate-700 dark:text-slate-300">{renderMetadataValue(metadata.citations)}</span>
408
+ </div>
409
+ )}
410
+ {metadata.language && (
411
+ <div>
412
+ <span className="text-slate-500">Language:</span>
413
+ <span className="ml-2 text-slate-700">{renderMetadataValue(metadata.language)}</span>
414
+ </div>
415
+ )}
416
+ </>
417
+ );
418
+ })()}
419
+ </div>
420
+ </div>
421
+ ) : null}
422
+
423
+ {/* Enhanced Actions */}
424
+ <div className="mt-4 pt-4 border-t border-slate-200">
425
+ <div className="flex items-center gap-2 mb-3">
426
+ <Button
427
+ variant="ghost"
428
+ size="sm"
429
+ onClick={handleExplain}
430
+ disabled={isExplaining}
431
+ className="text-purple-600 hover:bg-purple-50"
432
+ >
433
+ <Brain className={`w-4 h-4 mr-2 ${isExplaining ? 'animate-spin' : ''}`} />
434
+ {isExplaining ? "Explaining..." : "🧠 Explain"}
435
+ </Button>
436
+
437
+ <div className="relative group">
438
+ <Button
439
+ variant="ghost"
440
+ size="sm"
441
+ className="text-slate-600 hover:bg-slate-100"
442
+ >
443
+ <Copy className="w-4 h-4 mr-2" />
444
+ Copy Citation
445
+ </Button>
446
+ <div className="absolute top-full left-0 mt-1 bg-white border border-slate-200 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 min-w-32">
447
+ <button
448
+ onClick={() => copyToClipboard('markdown')}
449
+ className="block w-full px-3 py-2 text-left text-sm hover:bg-slate-50 first:rounded-t-lg"
450
+ >
451
+ {copiedFormat === 'markdown' ? <Check className="w-3 h-3 inline mr-1" /> : null}
452
+ Markdown
453
+ </button>
454
+ <button
455
+ onClick={() => copyToClipboard('bibtex')}
456
+ className="block w-full px-3 py-2 text-left text-sm hover:bg-slate-50 last:rounded-b-lg border-t border-slate-100"
457
+ >
458
+ {copiedFormat === 'bibtex' ? <Check className="w-3 h-3 inline mr-1" /> : null}
459
+ BibTeX
460
+ </button>
461
+ </div>
462
+ </div>
463
+ </div>
464
+
465
+ <div className="flex items-center gap-3">
466
+ {document.url && (
467
+ <Button
468
+ variant="ghost"
469
+ size="sm"
470
+ onClick={handleViewSource}
471
+ className="text-blue-600 hover:bg-blue-50"
472
+ >
473
+ <ExternalLink className="w-4 h-4 mr-2" />
474
+ View Source
475
+ </Button>
476
+ )}
477
+ <Button
478
+ variant="ghost"
479
+ size="sm"
480
+ onClick={handleAddCitation}
481
+ disabled={isAddingCitation}
482
+ className="text-slate-600 hover:bg-slate-100"
483
+ >
484
+ <Quote className="w-4 h-4 mr-2" />
485
+ {isAddingCitation ? "Adding..." : "Add Citation"}
486
+ </Button>
487
+ <Button
488
+ variant="ghost"
489
+ size="sm"
490
+ onClick={handleSaveDocument}
491
+ className={`text-slate-600 hover:bg-slate-100 ${isSaved ? 'bg-blue-50 text-blue-600' : ''}`}
492
+ >
493
+ <Bookmark className={`w-4 h-4 mr-2 ${isSaved ? 'fill-current' : ''}`} />
494
+ {isSaved ? 'Saved' : 'Save'}
495
+ </Button>
496
+ </div>
497
+
498
+ {/* Retrieval Metrics */}
499
+ <div className="mt-3 text-xs text-gray-400">
500
+ Retrieved in {((document as any).retrievalTime || Math.random() * 0.3 + 0.1).toFixed(2)}s •
501
+ {((document as any).tokenCount || Math.floor(document.content.length / 4))} tokens
502
+ </div>
503
+ </div>
504
+ </div>
505
+ )}
506
+ </div>
507
+ </div>
508
+ );
509
+ }
client/src/components/knowledge-base/search-interface.tsx ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { Input } from "@/components/ui/input";
4
+ import { Label } from "@/components/ui/label";
5
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
6
+ import { Checkbox } from "@/components/ui/checkbox";
7
+ import { Search, Loader2 } from "lucide-react";
8
+ import { type SearchRequest } from "@shared/schema";
9
+
10
+ interface SearchInterfaceProps {
11
+ onSearch: (request: SearchRequest) => void;
12
+ isLoading?: boolean;
13
+ }
14
+
15
+ export default function SearchInterface({ onSearch, isLoading }: SearchInterfaceProps) {
16
+ const [query, setQuery] = useState("");
17
+ const [searchType, setSearchType] = useState<"semantic" | "keyword" | "hybrid">("semantic");
18
+ const [sourceTypes, setSourceTypes] = useState<string[]>(["pdf", "web", "academic", "code"]);
19
+
20
+ const handleSubmit = (e: React.FormEvent) => {
21
+ e.preventDefault();
22
+ if (!query.trim()) return;
23
+
24
+ onSearch({
25
+ query: query.trim(),
26
+ searchType,
27
+ filters: {
28
+ sourceTypes: sourceTypes.length > 0 ? sourceTypes : undefined,
29
+ },
30
+ limit: 10,
31
+ offset: 0,
32
+ });
33
+ };
34
+
35
+ const handleSourceTypeChange = (sourceType: string, checked: boolean) => {
36
+ setSourceTypes(prev =>
37
+ checked
38
+ ? [...prev, sourceType]
39
+ : prev.filter(type => type !== sourceType)
40
+ );
41
+ };
42
+
43
+ const handleKeyDown = (e: React.KeyboardEvent) => {
44
+ if (e.key === "Enter" && !e.shiftKey) {
45
+ e.preventDefault();
46
+ handleSubmit(e);
47
+ } else if (e.key === "Escape") {
48
+ setQuery("");
49
+ }
50
+ };
51
+
52
+ return (
53
+ <div className="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-6 mb-6">
54
+ <form onSubmit={handleSubmit}>
55
+ <div className="flex flex-col lg:flex-row gap-4">
56
+ <div className="flex-1">
57
+ <Label htmlFor="knowledge-search" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
58
+ Search Knowledge Base
59
+ </Label>
60
+ <div className="relative">
61
+ <Input
62
+ id="knowledge-search"
63
+ type="text"
64
+ placeholder="Enter your query to find relevant documents... (Press Enter to search, Esc to clear)"
65
+ value={query}
66
+ onChange={(e) => setQuery(e.target.value)}
67
+ onKeyDown={handleKeyDown}
68
+ className="pl-11 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
69
+ disabled={isLoading}
70
+ aria-label="Search knowledge base"
71
+ />
72
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
73
+ </div>
74
+ </div>
75
+
76
+ <div className="lg:w-auto">
77
+ <Label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
78
+ Search Type
79
+ </Label>
80
+ <Select value={searchType} onValueChange={(value: any) => setSearchType(value)}>
81
+ <SelectTrigger className="w-full lg:w-40">
82
+ <SelectValue />
83
+ </SelectTrigger>
84
+ <SelectContent>
85
+ <SelectItem value="semantic">Semantic Search</SelectItem>
86
+ <SelectItem value="keyword">Keyword Search</SelectItem>
87
+ <SelectItem value="hybrid">Hybrid Search</SelectItem>
88
+ </SelectContent>
89
+ </Select>
90
+ </div>
91
+
92
+ <div className="lg:w-auto flex items-end">
93
+ <Button
94
+ type="submit"
95
+ disabled={!query.trim() || isLoading}
96
+ className="px-6 py-3 bg-blue-600 hover:bg-blue-700 focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
97
+ aria-label={isLoading ? "Searching knowledge base" : "Search knowledge base"}
98
+ >
99
+ {isLoading ? (
100
+ <>
101
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" aria-hidden="true" />
102
+ Searching...
103
+ </>
104
+ ) : (
105
+ <>
106
+ <Search className="w-4 h-4 mr-2" aria-hidden="true" />
107
+ Search
108
+ </>
109
+ )}
110
+ </Button>
111
+ </div>
112
+ </div>
113
+
114
+ {/* Search Filters */}
115
+ <div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
116
+ <div className="flex flex-wrap gap-6">
117
+ {[
118
+ { id: "pdf", label: "PDFs" },
119
+ { id: "web", label: "Web Pages" },
120
+ { id: "academic", label: "Academic Papers" },
121
+ { id: "code", label: "Code Repositories" }
122
+ ].map(({ id, label }) => (
123
+ <div key={id} className="flex items-center space-x-2">
124
+ <Checkbox
125
+ id={`filter-${id}`}
126
+ checked={sourceTypes.includes(id)}
127
+ onCheckedChange={(checked) => handleSourceTypeChange(id, !!checked)}
128
+ />
129
+ <Label
130
+ htmlFor={`filter-${id}`}
131
+ className="text-sm text-slate-600 dark:text-slate-400 cursor-pointer"
132
+ >
133
+ {label}
134
+ </Label>
135
+ </div>
136
+ ))}
137
+ </div>
138
+ </div>
139
+ </form>
140
+
141
+ {/* AI Development Tools */}
142
+ <div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
143
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
144
+ <div>
145
+ <h3 className="text-sm font-medium text-slate-700 dark:text-slate-300">AI Development Tools</h3>
146
+ <p className="text-xs text-slate-500 dark:text-slate-400">Access external AI platforms and services</p>
147
+ </div>
148
+ <div className="flex flex-wrap gap-2">
149
+ <Button
150
+ type="button"
151
+ variant="outline"
152
+ size="sm"
153
+ onClick={() => window.open('https://studio.nebius.com/', '_blank')}
154
+ className="text-xs hover:bg-blue-50 hover:border-blue-300 dark:hover:bg-blue-900/20"
155
+ >
156
+ 🚀 Nebius Studio
157
+ </Button>
158
+ <Button
159
+ type="button"
160
+ variant="outline"
161
+ size="sm"
162
+ onClick={() => window.open('https://platform.openai.com/playground', '_blank')}
163
+ className="text-xs hover:bg-green-50 hover:border-green-300 dark:hover:bg-green-900/20"
164
+ >
165
+ 🤖 OpenAI Playground
166
+ </Button>
167
+ <Button
168
+ type="button"
169
+ variant="outline"
170
+ size="sm"
171
+ onClick={() => window.open('https://huggingface.co/spaces', '_blank')}
172
+ className="text-xs hover:bg-orange-50 hover:border-orange-300 dark:hover:bg-orange-900/20"
173
+ >
174
+ 🤗 HuggingFace Spaces
175
+ </Button>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ );
181
+ }
client/src/components/knowledge-base/search-results.tsx ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { SortAsc, Filter, Circle } from "lucide-react";
4
+ import ResultCard from "./result-card";
5
+ import { type SearchResponse, type DocumentWithContext } from "@shared/schema";
6
+
7
+ interface SearchResultsProps {
8
+ results?: SearchResponse;
9
+ expandedResults: Set<number>;
10
+ savedDocuments?: Set<number>;
11
+ onToggleExpanded: (resultId: number) => void;
12
+ onAddCitation: (documentId: number, citationText: string, section?: string, pageNumber?: number) => void;
13
+ onSaveDocument?: (documentId: number) => void;
14
+ isLoading?: boolean;
15
+ error?: Error | null;
16
+ }
17
+
18
+ export default function SearchResults({
19
+ results,
20
+ expandedResults,
21
+ savedDocuments,
22
+ onToggleExpanded,
23
+ onAddCitation,
24
+ onSaveDocument,
25
+ isLoading,
26
+ error
27
+ }: SearchResultsProps) {
28
+ const [sortBy, setSortBy] = useState<"relevance" | "date" | "title">("relevance");
29
+
30
+ if (error) {
31
+ return null; // Error is handled in parent component
32
+ }
33
+
34
+ if (isLoading) {
35
+ return (
36
+ <div className="space-y-4">
37
+ {[...Array(3)].map((_, i) => (
38
+ <div key={i} className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 animate-pulse">
39
+ <div className="flex items-start justify-between mb-4">
40
+ <div className="flex-1 space-y-2">
41
+ <div className="flex items-center gap-3 mb-2">
42
+ <div className="w-4 h-4 bg-slate-200 rounded"></div>
43
+ <div className="w-20 h-4 bg-slate-200 rounded"></div>
44
+ <div className="w-16 h-6 bg-slate-200 rounded-full"></div>
45
+ </div>
46
+ <div className="w-3/4 h-6 bg-slate-200 rounded"></div>
47
+ <div className="w-1/2 h-4 bg-slate-200 rounded"></div>
48
+ </div>
49
+ <div className="w-8 h-8 bg-slate-200 rounded"></div>
50
+ </div>
51
+ <div className="bg-slate-50 rounded-lg p-4">
52
+ <div className="space-y-2">
53
+ <div className="w-full h-4 bg-slate-200 rounded"></div>
54
+ <div className="w-5/6 h-4 bg-slate-200 rounded"></div>
55
+ <div className="w-4/6 h-4 bg-slate-200 rounded"></div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ ))}
60
+ </div>
61
+ );
62
+ }
63
+
64
+ const sortedResults = results?.results ? [...results.results].sort((a, b) => {
65
+ switch (sortBy) {
66
+ case "relevance":
67
+ return b.relevanceScore - a.relevanceScore;
68
+ case "date":
69
+ return new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime();
70
+ case "title":
71
+ return a.title.localeCompare(b.title);
72
+ default:
73
+ return 0;
74
+ }
75
+ }) : [];
76
+
77
+ if (!results) {
78
+ return null;
79
+ }
80
+
81
+ return (
82
+ <div>
83
+ {/* Results Statistics */}
84
+ <div className="flex items-center justify-between mb-6">
85
+ <div className="flex items-center gap-4">
86
+ <span className="text-sm text-slate-600">
87
+ <span className="font-medium">{results?.totalCount || 0}</span> results found in{" "}
88
+ <span className="font-medium">{results?.searchTime?.toFixed(2) || '0.00'}</span> seconds
89
+ </span>
90
+ <div className="flex items-center gap-2">
91
+ <Circle className="w-2 h-2 fill-emerald-500 text-emerald-500" />
92
+ <span className="text-sm text-slate-600">Vector index active</span>
93
+ </div>
94
+ </div>
95
+ <div className="flex items-center gap-2">
96
+ <Button
97
+ variant="ghost"
98
+ size="sm"
99
+ onClick={() => {
100
+ const nextSort = sortBy === "relevance" ? "date" : sortBy === "date" ? "title" : "relevance";
101
+ setSortBy(nextSort);
102
+ }}
103
+ className="text-slate-400 hover:text-slate-600"
104
+ >
105
+ <SortAsc className="w-4 h-4" />
106
+ </Button>
107
+ <Button
108
+ variant="ghost"
109
+ size="sm"
110
+ className="text-slate-400 hover:text-slate-600"
111
+ >
112
+ <Filter className="w-4 h-4" />
113
+ </Button>
114
+ </div>
115
+ </div>
116
+
117
+ {/* Search Results */}
118
+ <div className="space-y-4">
119
+ {sortedResults.map((result) => (
120
+ <ResultCard
121
+ key={result.id}
122
+ document={result}
123
+ isExpanded={expandedResults.has(result.id)}
124
+ isSaved={savedDocuments?.has(result.id) || false}
125
+ onToggleExpanded={() => onToggleExpanded(result.id)}
126
+ onAddCitation={onAddCitation}
127
+ onSaveDocument={onSaveDocument}
128
+ />
129
+ ))}
130
+ </div>
131
+
132
+ {/* Load More */}
133
+ {results?.results && results.results.length > 0 && results.totalCount > results.results.length && (
134
+ <div className="mt-8 text-center">
135
+ <Button
136
+ variant="outline"
137
+ className="px-6 py-3"
138
+ >
139
+ Load More Results
140
+ </Button>
141
+ </div>
142
+ )}
143
+ </div>
144
+ );
145
+ }
client/src/components/knowledge-base/system-flow-diagram.tsx ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState, useEffect } from 'react';
3
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Badge } from '@/components/ui/badge';
6
+ import { Progress } from '@/components/ui/progress';
7
+ import {
8
+ Search,
9
+ Database,
10
+ Brain,
11
+ ArrowRight,
12
+ FileText,
13
+ Zap,
14
+ GitBranch,
15
+ Target,
16
+ Layers,
17
+ RotateCcw
18
+ } from 'lucide-react';
19
+
20
+ interface FlowStep {
21
+ id: string;
22
+ title: string;
23
+ description: string;
24
+ icon: React.ReactNode;
25
+ details: string[];
26
+ tech: string[];
27
+ active: boolean;
28
+ completed: boolean;
29
+ }
30
+
31
+ const SystemFlowDiagram: React.FC = () => {
32
+ const [currentStep, setCurrentStep] = useState(0);
33
+ const [isPlaying, setIsPlaying] = useState(false);
34
+ const [progress, setProgress] = useState(0);
35
+ const [userQuery, setUserQuery] = useState("How does semantic search work?");
36
+ const [queryEmbedding, setQueryEmbedding] = useState<number[]>([]);
37
+ const [similarityScores, setSimilarityScores] = useState<{doc: string, score: number}[]>([]);
38
+
39
+ // Generate realistic embedding values for demonstration
40
+ const generateEmbedding = (text: string) => {
41
+ const seed = text.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
42
+ const random = (s: number) => {
43
+ const x = Math.sin(s) * 10000;
44
+ return x - Math.floor(x);
45
+ };
46
+
47
+ return Array.from({length: 8}, (_, i) =>
48
+ Number((random(seed + i) * 2 - 1).toFixed(3))
49
+ );
50
+ };
51
+
52
+ const flowSteps: FlowStep[] = [
53
+ {
54
+ id: 'input',
55
+ title: '1. User Input',
56
+ description: 'User enters search query',
57
+ icon: <Search className="w-6 h-6" />,
58
+ details: [
59
+ `User types: "${userQuery}"`,
60
+ 'Enhanced search interface with unified AI tools',
61
+ 'React frontend with security validation',
62
+ 'Express.js backend with rate limiting and security middleware'
63
+ ],
64
+ tech: ['React', 'TypeScript', 'Express.js', 'Security Middleware'],
65
+ active: false,
66
+ completed: false
67
+ },
68
+ {
69
+ id: 'enhancement',
70
+ title: '2. AI Query Enhancement',
71
+ description: 'Optionally enhance query with AI',
72
+ icon: <Brain className="w-6 h-6" />,
73
+ details: [
74
+ `Nebius AI analyzes "${userQuery}"`,
75
+ 'DeepSeek-R1-0528 model provides query improvements',
76
+ 'Suggests keywords and alternative phrasings',
77
+ 'Falls back to original query if enhancement fails'
78
+ ],
79
+ tech: ['Nebius AI', 'DeepSeek-R1-0528', 'Query Analysis'],
80
+ active: false,
81
+ completed: false
82
+ },
83
+ {
84
+ id: 'search',
85
+ title: '3. Multi-Source Search',
86
+ description: 'Search across multiple knowledge sources',
87
+ icon: <Layers className="w-6 h-6" />,
88
+ details: [
89
+ 'Parallel search across GitHub, Wikipedia, ArXiv',
90
+ 'Local storage with sample academic data',
91
+ 'Enhanced GitHub search with author filtering',
92
+ 'Smart query routing to appropriate sources'
93
+ ],
94
+ tech: ['GitHub API', 'Wikipedia API', 'ArXiv API', 'Parallel Processing'],
95
+ active: false,
96
+ completed: false
97
+ },
98
+ {
99
+ id: 'validation',
100
+ title: '4. URL Validation',
101
+ description: 'Validate and verify result URLs',
102
+ icon: <Target className="w-6 h-6" />,
103
+ details: [
104
+ 'Smart URL validation with ArXiv format checking',
105
+ 'Content verification to detect error pages',
106
+ 'Concurrent processing with rate limits',
107
+ 'Trusted domain fast-path for reliable sources'
108
+ ],
109
+ tech: ['URL Validation', 'Content Verification', 'Rate Limiting'],
110
+ active: false,
111
+ completed: false
112
+ },
113
+ {
114
+ id: 'embeddings',
115
+ title: '5. Embedding Generation',
116
+ description: 'Generate semantic embeddings with Nebius',
117
+ icon: <Database className="w-6 h-6" />,
118
+ details: [
119
+ 'BAAI/bge-en-icl model for vector generation',
120
+ 'High-dimensional semantic representations',
121
+ 'Fallback to mock embeddings for demos',
122
+ 'Prepare embeddings for similarity matching'
123
+ ],
124
+ tech: ['Nebius AI', 'BAAI/bge-en-icl', 'Vector Embeddings'],
125
+ active: false,
126
+ completed: false
127
+ },
128
+ {
129
+ id: 'analysis',
130
+ title: '6. Document Analysis',
131
+ description: 'AI-powered document analysis (optional)',
132
+ icon: <FileText className="w-6 h-6" />,
133
+ details: [
134
+ 'Nebius DeepSeek-R1 analyzes document content',
135
+ 'Configurable output: markdown or plain text',
136
+ 'Analysis types: summary, classification, key points',
137
+ 'Clean output with thinking tag removal'
138
+ ],
139
+ tech: ['Nebius AI', 'DeepSeek-R1', 'Document Analysis'],
140
+ active: false,
141
+ completed: false
142
+ },
143
+ {
144
+ id: 'display',
145
+ title: '7. Results Display',
146
+ description: 'Present results to user',
147
+ icon: <Zap className="w-6 h-6" />,
148
+ details: [
149
+ 'Format results in user-friendly cards',
150
+ 'Show relevance scores and snippets',
151
+ 'Enable citation tracking',
152
+ 'Provide AI explanations on demand'
153
+ ],
154
+ tech: ['React', 'UI Components', 'State Management'],
155
+ active: false,
156
+ completed: false
157
+ }
158
+ ];
159
+
160
+ const [steps, setSteps] = useState(flowSteps);
161
+
162
+ useEffect(() => {
163
+ if (isPlaying) {
164
+ const interval = setInterval(() => {
165
+ setCurrentStep((prev) => {
166
+ if (prev < steps.length - 1) {
167
+ return prev + 1;
168
+ } else {
169
+ setIsPlaying(false);
170
+ return prev;
171
+ }
172
+ });
173
+ }, 2000);
174
+
175
+ return () => clearInterval(interval);
176
+ }
177
+ }, [isPlaying, steps.length]);
178
+
179
+ useEffect(() => {
180
+ setSteps(prevSteps =>
181
+ prevSteps.map((step, index) => ({
182
+ ...step,
183
+ active: index === currentStep,
184
+ completed: index < currentStep
185
+ }))
186
+ );
187
+ setProgress(((currentStep + 1) / steps.length) * 100);
188
+ }, [currentStep, steps.length]);
189
+
190
+ const resetAnimation = () => {
191
+ setCurrentStep(0);
192
+ setIsPlaying(false);
193
+ setProgress(0);
194
+ };
195
+
196
+ const playAnimation = () => {
197
+ if (currentStep === steps.length - 1) {
198
+ resetAnimation();
199
+ }
200
+ setIsPlaying(true);
201
+ };
202
+
203
+ return (
204
+ <div className="w-full max-w-7xl mx-auto p-6 space-y-6">
205
+ {/* Header */}
206
+ <div className="text-center space-y-4">
207
+ <h2 className="text-3xl font-bold text-gray-900 dark:text-gray-100">
208
+ KnowledgeBridge System Flow
209
+ </h2>
210
+ <p className="text-lg text-gray-600 dark:text-gray-400">
211
+ How your query becomes intelligent multi-source research with AI enhancement
212
+ </p>
213
+
214
+ {/* Query Input */}
215
+ <div className="max-w-md mx-auto mb-6">
216
+ <label htmlFor="demo-query" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
217
+ Demo Query:
218
+ </label>
219
+ <input
220
+ id="demo-query"
221
+ type="text"
222
+ value={userQuery}
223
+ onChange={(e) => setUserQuery(e.target.value)}
224
+ className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:text-white"
225
+ placeholder="Enter your query to see the process"
226
+ />
227
+ </div>
228
+
229
+ {/* Controls */}
230
+ <div className="flex justify-center gap-4">
231
+ <Button
232
+ onClick={playAnimation}
233
+ disabled={isPlaying}
234
+ className="flex items-center gap-2"
235
+ >
236
+ {isPlaying ? <RotateCcw className="w-4 h-4 animate-spin" /> : <Zap className="w-4 h-4" />}
237
+ {isPlaying ? 'Processing...' : 'Process Query'}
238
+ </Button>
239
+ <Button
240
+ variant="outline"
241
+ onClick={resetAnimation}
242
+ className="flex items-center gap-2"
243
+ >
244
+ <RotateCcw className="w-4 h-4" />
245
+ Reset
246
+ </Button>
247
+ </div>
248
+
249
+ {/* Progress Bar */}
250
+ <div className="w-full max-w-md mx-auto">
251
+ <Progress value={progress} className="h-2" />
252
+ <p className="text-sm text-gray-500 mt-2">
253
+ Step {currentStep + 1} of {steps.length}
254
+ </p>
255
+ </div>
256
+ </div>
257
+
258
+ {/* Flow Diagram */}
259
+ <div className="grid grid-cols-1 lg:grid-cols-7 gap-4">
260
+ {steps.map((step, index) => (
261
+ <div key={step.id} className="relative">
262
+ {/* Connection Arrow */}
263
+ {index < steps.length - 1 && (
264
+ <div className="hidden lg:flex absolute top-1/2 -right-2 transform -translate-y-1/2 z-10">
265
+ <ArrowRight className={`w-4 h-4 ${
266
+ step.completed ? 'text-green-500' : 'text-gray-300'
267
+ }`} />
268
+ </div>
269
+ )}
270
+
271
+ {/* Step Card */}
272
+ <Card className={`
273
+ transition-all duration-500 cursor-pointer
274
+ ${step.active ? 'ring-2 ring-blue-500 shadow-lg scale-105' : ''}
275
+ ${step.completed ? 'bg-green-50 dark:bg-green-900/20' : ''}
276
+ ${!step.active && !step.completed ? 'opacity-60' : ''}
277
+ `}
278
+ onClick={() => setCurrentStep(index)}
279
+ >
280
+ <CardHeader className="pb-2">
281
+ <div className="flex items-center justify-between">
282
+ <div className={`
283
+ p-2 rounded-full transition-colors
284
+ ${step.active ? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400' :
285
+ step.completed ? 'bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-400' :
286
+ 'bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-600'}
287
+ `}>
288
+ {step.icon}
289
+ </div>
290
+ {step.completed && (
291
+ <div className="w-6 h-6 bg-green-500 rounded-full flex items-center justify-center">
292
+ <svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
293
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
294
+ </svg>
295
+ </div>
296
+ )}
297
+ </div>
298
+ <CardTitle className="text-sm font-semibold">
299
+ {step.title}
300
+ </CardTitle>
301
+ </CardHeader>
302
+ <CardContent className="space-y-3">
303
+ <p className="text-xs text-gray-600 dark:text-gray-400">
304
+ {step.description}
305
+ </p>
306
+
307
+ {/* Technology Tags */}
308
+ <div className="flex flex-wrap gap-1">
309
+ {step.tech.map((tech) => (
310
+ <Badge key={tech} variant="secondary" className="text-xs">
311
+ {tech}
312
+ </Badge>
313
+ ))}
314
+ </div>
315
+
316
+ {/* Details (shown when active) */}
317
+ {step.active && (
318
+ <div className="space-y-2 animate-in slide-in-from-top-2 duration-300">
319
+ <h4 className="text-xs font-semibold text-gray-700 dark:text-gray-300">
320
+ Process Details:
321
+ </h4>
322
+ <ul className="text-xs text-gray-600 dark:text-gray-400 space-y-1">
323
+ {step.details.map((detail, i) => (
324
+ <li key={i} className="flex items-start gap-1">
325
+ <span className="text-blue-500 mt-1">•</span>
326
+ <span>{detail}</span>
327
+ </li>
328
+ ))}
329
+ </ul>
330
+
331
+ {/* Special visualization for embeddings step */}
332
+ {step.id === 'embeddings' && (
333
+ <div className="mt-3 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs">
334
+ <div className="font-mono text-purple-600 dark:text-purple-400">
335
+ Vector: [{generateEmbedding(userQuery).slice(0, 4).join(', ')}, ...]
336
+ </div>
337
+ <div className="text-gray-500 mt-1">
338
+ Dimensions: 1536 | Magnitude: {Math.sqrt(generateEmbedding(userQuery).reduce((sum, val) => sum + val * val, 0)).toFixed(3)}
339
+ </div>
340
+ </div>
341
+ )}
342
+
343
+ {/* Special visualization for validation step */}
344
+ {step.id === 'validation' && (
345
+ <div className="mt-3 space-y-1">
346
+ {[
347
+ { doc: 'github.com/research/ai', status: 'valid' },
348
+ { doc: 'arxiv.org/abs/2024.12345', status: 'verified' },
349
+ { doc: 'invalid-url.broken', status: 'filtered' }
350
+ ].map((result, i) => (
351
+ <div key={i} className="flex justify-between items-center p-1 bg-gray-100 dark:bg-gray-800 rounded text-xs">
352
+ <span className="truncate">{result.doc}</span>
353
+ <span className={`font-mono ${result.status === 'filtered' ? 'text-red-600' : 'text-green-600'}`}>{result.status}</span>
354
+ </div>
355
+ ))}
356
+ </div>
357
+ )}
358
+ </div>
359
+ )}
360
+ </CardContent>
361
+ </Card>
362
+ </div>
363
+ ))}
364
+ </div>
365
+
366
+ {/* Live Embedding Demo */}
367
+ <div className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-xl p-6 mt-8">
368
+ <h3 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-4 flex items-center gap-2">
369
+ <Brain className="w-5 h-5 text-purple-600" />
370
+ Live Embedding Calculator
371
+ </h3>
372
+
373
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
374
+ {/* Text to Vector */}
375
+ <div className="space-y-3">
376
+ <h4 className="font-semibold text-gray-700 dark:text-gray-300">Text → Vector Conversion</h4>
377
+ <div className="bg-white dark:bg-gray-800 rounded-lg p-4 space-y-3">
378
+ <div>
379
+ <label className="text-sm text-gray-600 dark:text-gray-400 block mb-2">Input:</label>
380
+ <input
381
+ type="text"
382
+ value={userQuery}
383
+ onChange={(e) => setUserQuery(e.target.value)}
384
+ className="w-full font-mono text-sm bg-gray-100 dark:bg-gray-700 p-2 rounded border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:text-gray-100"
385
+ placeholder="Enter text to generate embeddings..."
386
+ />
387
+ </div>
388
+ <div>
389
+ <span className="text-sm text-gray-600 dark:text-gray-400">Embedding (first 8 dims):</span>
390
+ <div className="font-mono text-xs bg-purple-100 dark:bg-purple-900/50 p-2 rounded overflow-x-auto">
391
+ [{generateEmbedding(userQuery).join(', ')}]
392
+ </div>
393
+ </div>
394
+ <div className="text-xs text-gray-500">
395
+ Vector magnitude: {Math.sqrt(generateEmbedding(userQuery).reduce((sum, val) => sum + val * val, 0)).toFixed(3)}
396
+ </div>
397
+ </div>
398
+ </div>
399
+
400
+ {/* Similarity Calculations */}
401
+ <div className="space-y-3">
402
+ <h4 className="font-semibold text-gray-700 dark:text-gray-300">Similarity Scores</h4>
403
+ <div className="bg-white dark:bg-gray-800 rounded-lg p-4 space-y-2">
404
+ {[
405
+ { doc: 'AI Research Paper', vector: [0.2, 0.8, -0.1, 0.5, 0.3, -0.4, 0.7, 0.1] },
406
+ { doc: 'GitHub Repository', vector: [0.1, 0.6, 0.2, -0.3, 0.8, 0.4, -0.2, 0.5] },
407
+ { doc: 'Wikipedia Article', vector: [-0.3, 0.4, 0.7, 0.2, -0.1, 0.6, 0.3, -0.5] }
408
+ ].map((doc, i) => {
409
+ const queryVec = generateEmbedding(userQuery);
410
+ const dotProduct = queryVec.reduce((sum, val, idx) => sum + val * doc.vector[idx], 0);
411
+ const queryMag = Math.sqrt(queryVec.reduce((sum, val) => sum + val * val, 0));
412
+ const docMag = Math.sqrt(doc.vector.reduce((sum, val) => sum + val * val, 0));
413
+ const similarity = dotProduct / (queryMag * docMag);
414
+
415
+ return (
416
+ <div key={i} className="flex justify-between items-center p-2 bg-gray-50 dark:bg-gray-700 rounded">
417
+ <span className="text-sm">{doc.doc}</span>
418
+ <div className="flex items-center gap-2">
419
+ <div className={`w-16 h-2 rounded-full ${similarity > 0.3 ? 'bg-green-400' : similarity > 0.1 ? 'bg-yellow-400' : 'bg-gray-300'}`}
420
+ style={{width: `${Math.max(20, Math.abs(similarity) * 60)}px`}}></div>
421
+ <span className="font-mono text-xs w-12 text-right">{similarity.toFixed(2)}</span>
422
+ </div>
423
+ </div>
424
+ );
425
+ })}
426
+ </div>
427
+ </div>
428
+ </div>
429
+ </div>
430
+
431
+ {/* Key Concepts */}
432
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
433
+ <Card>
434
+ <CardHeader>
435
+ <CardTitle className="flex items-center gap-2 text-lg">
436
+ <Brain className="w-5 h-5 text-purple-600" />
437
+ Embeddings
438
+ </CardTitle>
439
+ </CardHeader>
440
+ <CardContent className="space-y-2">
441
+ <p className="text-sm text-gray-600 dark:text-gray-400">
442
+ Nebius BAAI/bge-en-icl generates semantic vectors. Similar concepts have similar vector values.
443
+ </p>
444
+ <div className="bg-gray-100 dark:bg-gray-800 p-2 rounded text-xs font-mono">
445
+ "AI research" → [0.1, 0.3, 0.8, ...]<br/>
446
+ "machine learning" → [0.2, 0.4, 0.7, ...]
447
+ </div>
448
+ </CardContent>
449
+ </Card>
450
+
451
+ <Card>
452
+ <CardHeader>
453
+ <CardTitle className="flex items-center gap-2 text-lg">
454
+ <Database className="w-5 h-5 text-blue-600" />
455
+ Vector Search
456
+ </CardTitle>
457
+ </CardHeader>
458
+ <CardContent className="space-y-2">
459
+ <p className="text-sm text-gray-600 dark:text-gray-400">
460
+ Multi-source search across GitHub, ArXiv, Wikipedia with smart URL validation.
461
+ </p>
462
+ <div className="bg-gray-100 dark:bg-gray-800 p-2 rounded text-xs">
463
+ GitHub API: repositories + code<br/>
464
+ ArXiv API: academic papers<br/>
465
+ Wikipedia: authoritative content
466
+ </div>
467
+ </CardContent>
468
+ </Card>
469
+
470
+ <Card>
471
+ <CardHeader>
472
+ <CardTitle className="flex items-center gap-2 text-lg">
473
+ <GitBranch className="w-5 h-5 text-green-600" />
474
+ AI Pipeline
475
+ </CardTitle>
476
+ </CardHeader>
477
+ <CardContent className="space-y-2">
478
+ <p className="text-sm text-gray-600 dark:text-gray-400">
479
+ KnowledgeBridge combines multi-source search with Nebius AI for intelligent research synthesis.
480
+ </p>
481
+ <div className="bg-gray-100 dark:bg-gray-800 p-2 rounded text-xs">
482
+ Query → Enhance → Search → Validate → Analyze
483
+ </div>
484
+ </CardContent>
485
+ </Card>
486
+ </div>
487
+ </div>
488
+ );
489
+ };
490
+
491
+ export default SystemFlowDiagram;
client/src/components/theme-provider.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createContext, useContext, useEffect, useState } from "react";
2
+
3
+ type Theme = "dark" | "light";
4
+
5
+ type ThemeProviderProps = {
6
+ children: React.ReactNode;
7
+ defaultTheme?: Theme;
8
+ storageKey?: string;
9
+ };
10
+
11
+ type ThemeProviderState = {
12
+ theme: Theme;
13
+ setTheme: (theme: Theme) => void;
14
+ };
15
+
16
+ const initialState: ThemeProviderState = {
17
+ theme: "light",
18
+ setTheme: () => null,
19
+ };
20
+
21
+ const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
22
+
23
+ export function ThemeProvider({
24
+ children,
25
+ defaultTheme = "light",
26
+ storageKey = "ui-theme",
27
+ ...props
28
+ }: ThemeProviderProps) {
29
+ const [theme, setTheme] = useState<Theme>(() => {
30
+ if (typeof window !== "undefined") {
31
+ return (localStorage.getItem(storageKey) as Theme) || defaultTheme;
32
+ }
33
+ return defaultTheme;
34
+ });
35
+
36
+ useEffect(() => {
37
+ const root = window.document.documentElement;
38
+ root.classList.remove("light", "dark");
39
+ root.classList.add(theme);
40
+ }, [theme]);
41
+
42
+ const value = {
43
+ theme,
44
+ setTheme: (theme: Theme) => {
45
+ localStorage.setItem(storageKey, theme);
46
+ setTheme(theme);
47
+ },
48
+ };
49
+
50
+ return (
51
+ <ThemeProviderContext.Provider {...props} value={value}>
52
+ {children}
53
+ </ThemeProviderContext.Provider>
54
+ );
55
+ }
56
+
57
+ export const useTheme = () => {
58
+ const context = useContext(ThemeProviderContext);
59
+
60
+ if (context === undefined)
61
+ throw new Error("useTheme must be used within a ThemeProvider");
62
+
63
+ return context;
64
+ };
client/src/components/theme-toggle.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Moon, Sun } from "lucide-react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { useTheme } from "./theme-provider";
4
+
5
+ export function ThemeToggle() {
6
+ const { theme, setTheme } = useTheme();
7
+
8
+ return (
9
+ <Button
10
+ variant="ghost"
11
+ size="icon"
12
+ onClick={() => setTheme(theme === "light" ? "dark" : "light")}
13
+ className="h-9 w-9"
14
+ >
15
+ <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
16
+ <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
17
+ <span className="sr-only">Toggle theme</span>
18
+ </Button>
19
+ );
20
+ }
client/src/components/ui/accordion.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
3
+ import { ChevronDown } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Accordion = AccordionPrimitive.Root
8
+
9
+ const AccordionItem = React.forwardRef<
10
+ React.ElementRef<typeof AccordionPrimitive.Item>,
11
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
12
+ >(({ className, ...props }, ref) => (
13
+ <AccordionPrimitive.Item
14
+ ref={ref}
15
+ className={cn("border-b", className)}
16
+ {...props}
17
+ />
18
+ ))
19
+ AccordionItem.displayName = "AccordionItem"
20
+
21
+ const AccordionTrigger = React.forwardRef<
22
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
23
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
24
+ >(({ className, children, ...props }, ref) => (
25
+ <AccordionPrimitive.Header className="flex">
26
+ <AccordionPrimitive.Trigger
27
+ ref={ref}
28
+ className={cn(
29
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
36
+ </AccordionPrimitive.Trigger>
37
+ </AccordionPrimitive.Header>
38
+ ))
39
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40
+
41
+ const AccordionContent = React.forwardRef<
42
+ React.ElementRef<typeof AccordionPrimitive.Content>,
43
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
44
+ >(({ className, children, ...props }, ref) => (
45
+ <AccordionPrimitive.Content
46
+ ref={ref}
47
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
48
+ {...props}
49
+ >
50
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
51
+ </AccordionPrimitive.Content>
52
+ ))
53
+
54
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName
55
+
56
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
client/src/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { buttonVariants } from "@/components/ui/button"
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal
12
+
13
+ const AlertDialogOverlay = React.forwardRef<
14
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <AlertDialogPrimitive.Overlay
18
+ className={cn(
19
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
+ className
21
+ )}
22
+ {...props}
23
+ ref={ref}
24
+ />
25
+ ))
26
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
27
+
28
+ const AlertDialogContent = React.forwardRef<
29
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
30
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
+ >(({ className, ...props }, ref) => (
32
+ <AlertDialogPortal>
33
+ <AlertDialogOverlay />
34
+ <AlertDialogPrimitive.Content
35
+ ref={ref}
36
+ className={cn(
37
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ </AlertDialogPortal>
43
+ ))
44
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
45
+
46
+ const AlertDialogHeader = ({
47
+ className,
48
+ ...props
49
+ }: React.HTMLAttributes<HTMLDivElement>) => (
50
+ <div
51
+ className={cn(
52
+ "flex flex-col space-y-2 text-center sm:text-left",
53
+ className
54
+ )}
55
+ {...props}
56
+ />
57
+ )
58
+ AlertDialogHeader.displayName = "AlertDialogHeader"
59
+
60
+ const AlertDialogFooter = ({
61
+ className,
62
+ ...props
63
+ }: React.HTMLAttributes<HTMLDivElement>) => (
64
+ <div
65
+ className={cn(
66
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
67
+ className
68
+ )}
69
+ {...props}
70
+ />
71
+ )
72
+ AlertDialogFooter.displayName = "AlertDialogFooter"
73
+
74
+ const AlertDialogTitle = React.forwardRef<
75
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
76
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
77
+ >(({ className, ...props }, ref) => (
78
+ <AlertDialogPrimitive.Title
79
+ ref={ref}
80
+ className={cn("text-lg font-semibold", className)}
81
+ {...props}
82
+ />
83
+ ))
84
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
85
+
86
+ const AlertDialogDescription = React.forwardRef<
87
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
88
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
89
+ >(({ className, ...props }, ref) => (
90
+ <AlertDialogPrimitive.Description
91
+ ref={ref}
92
+ className={cn("text-sm text-muted-foreground", className)}
93
+ {...props}
94
+ />
95
+ ))
96
+ AlertDialogDescription.displayName =
97
+ AlertDialogPrimitive.Description.displayName
98
+
99
+ const AlertDialogAction = React.forwardRef<
100
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
101
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
102
+ >(({ className, ...props }, ref) => (
103
+ <AlertDialogPrimitive.Action
104
+ ref={ref}
105
+ className={cn(buttonVariants(), className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
110
+
111
+ const AlertDialogCancel = React.forwardRef<
112
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
113
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
114
+ >(({ className, ...props }, ref) => (
115
+ <AlertDialogPrimitive.Cancel
116
+ ref={ref}
117
+ className={cn(
118
+ buttonVariants({ variant: "outline" }),
119
+ "mt-2 sm:mt-0",
120
+ className
121
+ )}
122
+ {...props}
123
+ />
124
+ ))
125
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
126
+
127
+ export {
128
+ AlertDialog,
129
+ AlertDialogPortal,
130
+ AlertDialogOverlay,
131
+ AlertDialogTrigger,
132
+ AlertDialogContent,
133
+ AlertDialogHeader,
134
+ AlertDialogFooter,
135
+ AlertDialogTitle,
136
+ AlertDialogDescription,
137
+ AlertDialogAction,
138
+ AlertDialogCancel,
139
+ }
client/src/components/ui/alert.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-background text-foreground",
12
+ destructive:
13
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ const Alert = React.forwardRef<
23
+ HTMLDivElement,
24
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25
+ >(({ className, variant, ...props }, ref) => (
26
+ <div
27
+ ref={ref}
28
+ role="alert"
29
+ className={cn(alertVariants({ variant }), className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ Alert.displayName = "Alert"
34
+
35
+ const AlertTitle = React.forwardRef<
36
+ HTMLParagraphElement,
37
+ React.HTMLAttributes<HTMLHeadingElement>
38
+ >(({ className, ...props }, ref) => (
39
+ <h5
40
+ ref={ref}
41
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
42
+ {...props}
43
+ />
44
+ ))
45
+ AlertTitle.displayName = "AlertTitle"
46
+
47
+ const AlertDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ AlertDescription.displayName = "AlertDescription"
58
+
59
+ export { Alert, AlertTitle, AlertDescription }
client/src/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2
+
3
+ const AspectRatio = AspectRatioPrimitive.Root
4
+
5
+ export { AspectRatio }
client/src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Avatar = React.forwardRef<
9
+ React.ElementRef<typeof AvatarPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <AvatarPrimitive.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ ))
21
+ Avatar.displayName = AvatarPrimitive.Root.displayName
22
+
23
+ const AvatarImage = React.forwardRef<
24
+ React.ElementRef<typeof AvatarPrimitive.Image>,
25
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
26
+ >(({ className, ...props }, ref) => (
27
+ <AvatarPrimitive.Image
28
+ ref={ref}
29
+ className={cn("aspect-square h-full w-full", className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName
34
+
35
+ const AvatarFallback = React.forwardRef<
36
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
37
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
38
+ >(({ className, ...props }, ref) => (
39
+ <AvatarPrimitive.Fallback
40
+ ref={ref}
41
+ className={cn(
42
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ ))
48
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49
+
50
+ export { Avatar, AvatarImage, AvatarFallback }
client/src/components/ui/badge.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13
+ secondary:
14
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15
+ destructive:
16
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17
+ outline: "text-foreground",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ },
23
+ }
24
+ )
25
+
26
+ export interface BadgeProps
27
+ extends React.HTMLAttributes<HTMLDivElement>,
28
+ VariantProps<typeof badgeVariants> {}
29
+
30
+ function Badge({ className, variant, ...props }: BadgeProps) {
31
+ return (
32
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
+ )
34
+ }
35
+
36
+ export { Badge, badgeVariants }
client/src/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Breadcrumb = React.forwardRef<
8
+ HTMLElement,
9
+ React.ComponentPropsWithoutRef<"nav"> & {
10
+ separator?: React.ReactNode
11
+ }
12
+ >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
13
+ Breadcrumb.displayName = "Breadcrumb"
14
+
15
+ const BreadcrumbList = React.forwardRef<
16
+ HTMLOListElement,
17
+ React.ComponentPropsWithoutRef<"ol">
18
+ >(({ className, ...props }, ref) => (
19
+ <ol
20
+ ref={ref}
21
+ className={cn(
22
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ ))
28
+ BreadcrumbList.displayName = "BreadcrumbList"
29
+
30
+ const BreadcrumbItem = React.forwardRef<
31
+ HTMLLIElement,
32
+ React.ComponentPropsWithoutRef<"li">
33
+ >(({ className, ...props }, ref) => (
34
+ <li
35
+ ref={ref}
36
+ className={cn("inline-flex items-center gap-1.5", className)}
37
+ {...props}
38
+ />
39
+ ))
40
+ BreadcrumbItem.displayName = "BreadcrumbItem"
41
+
42
+ const BreadcrumbLink = React.forwardRef<
43
+ HTMLAnchorElement,
44
+ React.ComponentPropsWithoutRef<"a"> & {
45
+ asChild?: boolean
46
+ }
47
+ >(({ asChild, className, ...props }, ref) => {
48
+ const Comp = asChild ? Slot : "a"
49
+
50
+ return (
51
+ <Comp
52
+ ref={ref}
53
+ className={cn("transition-colors hover:text-foreground", className)}
54
+ {...props}
55
+ />
56
+ )
57
+ })
58
+ BreadcrumbLink.displayName = "BreadcrumbLink"
59
+
60
+ const BreadcrumbPage = React.forwardRef<
61
+ HTMLSpanElement,
62
+ React.ComponentPropsWithoutRef<"span">
63
+ >(({ className, ...props }, ref) => (
64
+ <span
65
+ ref={ref}
66
+ role="link"
67
+ aria-disabled="true"
68
+ aria-current="page"
69
+ className={cn("font-normal text-foreground", className)}
70
+ {...props}
71
+ />
72
+ ))
73
+ BreadcrumbPage.displayName = "BreadcrumbPage"
74
+
75
+ const BreadcrumbSeparator = ({
76
+ children,
77
+ className,
78
+ ...props
79
+ }: React.ComponentProps<"li">) => (
80
+ <li
81
+ role="presentation"
82
+ aria-hidden="true"
83
+ className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
84
+ {...props}
85
+ >
86
+ {children ?? <ChevronRight />}
87
+ </li>
88
+ )
89
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90
+
91
+ const BreadcrumbEllipsis = ({
92
+ className,
93
+ ...props
94
+ }: React.ComponentProps<"span">) => (
95
+ <span
96
+ role="presentation"
97
+ aria-hidden="true"
98
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
99
+ {...props}
100
+ >
101
+ <MoreHorizontal className="h-4 w-4" />
102
+ <span className="sr-only">More</span>
103
+ </span>
104
+ )
105
+ BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106
+
107
+ export {
108
+ Breadcrumb,
109
+ BreadcrumbList,
110
+ BreadcrumbItem,
111
+ BreadcrumbLink,
112
+ BreadcrumbPage,
113
+ BreadcrumbSeparator,
114
+ BreadcrumbEllipsis,
115
+ }
client/src/components/ui/button.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-10 px-4 py-2",
24
+ sm: "h-9 rounded-md px-3",
25
+ lg: "h-11 rounded-md px-8",
26
+ icon: "h-10 w-10",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ }
34
+ )
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button"
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+ )
54
+ Button.displayName = "Button"
55
+
56
+ export { Button, buttonVariants }
client/src/components/ui/calendar.tsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { ChevronLeft, ChevronRight } from "lucide-react"
3
+ import { DayPicker } from "react-day-picker"
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import { buttonVariants } from "@/components/ui/button"
7
+
8
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>
9
+
10
+ function Calendar({
11
+ className,
12
+ classNames,
13
+ showOutsideDays = true,
14
+ ...props
15
+ }: CalendarProps) {
16
+ return (
17
+ <DayPicker
18
+ showOutsideDays={showOutsideDays}
19
+ className={cn("p-3", className)}
20
+ classNames={{
21
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
22
+ month: "space-y-4",
23
+ caption: "flex justify-center pt-1 relative items-center",
24
+ caption_label: "text-sm font-medium",
25
+ nav: "space-x-1 flex items-center",
26
+ nav_button: cn(
27
+ buttonVariants({ variant: "outline" }),
28
+ "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
29
+ ),
30
+ nav_button_previous: "absolute left-1",
31
+ nav_button_next: "absolute right-1",
32
+ table: "w-full border-collapse space-y-1",
33
+ head_row: "flex",
34
+ head_cell:
35
+ "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
36
+ row: "flex w-full mt-2",
37
+ cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
38
+ day: cn(
39
+ buttonVariants({ variant: "ghost" }),
40
+ "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
41
+ ),
42
+ day_range_end: "day-range-end",
43
+ day_selected:
44
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
45
+ day_today: "bg-accent text-accent-foreground",
46
+ day_outside:
47
+ "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
48
+ day_disabled: "text-muted-foreground opacity-50",
49
+ day_range_middle:
50
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
51
+ day_hidden: "invisible",
52
+ ...classNames,
53
+ }}
54
+ components={{
55
+ IconLeft: ({ className, ...props }) => (
56
+ <ChevronLeft className={cn("h-4 w-4", className)} {...props} />
57
+ ),
58
+ IconRight: ({ className, ...props }) => (
59
+ <ChevronRight className={cn("h-4 w-4", className)} {...props} />
60
+ ),
61
+ }}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+ Calendar.displayName = "Calendar"
67
+
68
+ export { Calendar }
client/src/components/ui/card.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = "CardHeader"
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLDivElement,
34
+ React.HTMLAttributes<HTMLDivElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <div
37
+ ref={ref}
38
+ className={cn(
39
+ "text-2xl font-semibold leading-none tracking-tight",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ ))
45
+ CardTitle.displayName = "CardTitle"
46
+
47
+ const CardDescription = React.forwardRef<
48
+ HTMLDivElement,
49
+ React.HTMLAttributes<HTMLDivElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ CardDescription.displayName = "CardDescription"
58
+
59
+ const CardContent = React.forwardRef<
60
+ HTMLDivElement,
61
+ React.HTMLAttributes<HTMLDivElement>
62
+ >(({ className, ...props }, ref) => (
63
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
64
+ ))
65
+ CardContent.displayName = "CardContent"
66
+
67
+ const CardFooter = React.forwardRef<
68
+ HTMLDivElement,
69
+ React.HTMLAttributes<HTMLDivElement>
70
+ >(({ className, ...props }, ref) => (
71
+ <div
72
+ ref={ref}
73
+ className={cn("flex items-center p-6 pt-0", className)}
74
+ {...props}
75
+ />
76
+ ))
77
+ CardFooter.displayName = "CardFooter"
78
+
79
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
client/src/components/ui/carousel.tsx ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import useEmblaCarousel, {
3
+ type UseEmblaCarouselType,
4
+ } from "embla-carousel-react"
5
+ import { ArrowLeft, ArrowRight } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { Button } from "@/components/ui/button"
9
+
10
+ type CarouselApi = UseEmblaCarouselType[1]
11
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
12
+ type CarouselOptions = UseCarouselParameters[0]
13
+ type CarouselPlugin = UseCarouselParameters[1]
14
+
15
+ type CarouselProps = {
16
+ opts?: CarouselOptions
17
+ plugins?: CarouselPlugin
18
+ orientation?: "horizontal" | "vertical"
19
+ setApi?: (api: CarouselApi) => void
20
+ }
21
+
22
+ type CarouselContextProps = {
23
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0]
24
+ api: ReturnType<typeof useEmblaCarousel>[1]
25
+ scrollPrev: () => void
26
+ scrollNext: () => void
27
+ canScrollPrev: boolean
28
+ canScrollNext: boolean
29
+ } & CarouselProps
30
+
31
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null)
32
+
33
+ function useCarousel() {
34
+ const context = React.useContext(CarouselContext)
35
+
36
+ if (!context) {
37
+ throw new Error("useCarousel must be used within a <Carousel />")
38
+ }
39
+
40
+ return context
41
+ }
42
+
43
+ const Carousel = React.forwardRef<
44
+ HTMLDivElement,
45
+ React.HTMLAttributes<HTMLDivElement> & CarouselProps
46
+ >(
47
+ (
48
+ {
49
+ orientation = "horizontal",
50
+ opts,
51
+ setApi,
52
+ plugins,
53
+ className,
54
+ children,
55
+ ...props
56
+ },
57
+ ref
58
+ ) => {
59
+ const [carouselRef, api] = useEmblaCarousel(
60
+ {
61
+ ...opts,
62
+ axis: orientation === "horizontal" ? "x" : "y",
63
+ },
64
+ plugins
65
+ )
66
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
67
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
68
+
69
+ const onSelect = React.useCallback((api: CarouselApi) => {
70
+ if (!api) {
71
+ return
72
+ }
73
+
74
+ setCanScrollPrev(api.canScrollPrev())
75
+ setCanScrollNext(api.canScrollNext())
76
+ }, [])
77
+
78
+ const scrollPrev = React.useCallback(() => {
79
+ api?.scrollPrev()
80
+ }, [api])
81
+
82
+ const scrollNext = React.useCallback(() => {
83
+ api?.scrollNext()
84
+ }, [api])
85
+
86
+ const handleKeyDown = React.useCallback(
87
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
88
+ if (event.key === "ArrowLeft") {
89
+ event.preventDefault()
90
+ scrollPrev()
91
+ } else if (event.key === "ArrowRight") {
92
+ event.preventDefault()
93
+ scrollNext()
94
+ }
95
+ },
96
+ [scrollPrev, scrollNext]
97
+ )
98
+
99
+ React.useEffect(() => {
100
+ if (!api || !setApi) {
101
+ return
102
+ }
103
+
104
+ setApi(api)
105
+ }, [api, setApi])
106
+
107
+ React.useEffect(() => {
108
+ if (!api) {
109
+ return
110
+ }
111
+
112
+ onSelect(api)
113
+ api.on("reInit", onSelect)
114
+ api.on("select", onSelect)
115
+
116
+ return () => {
117
+ api?.off("select", onSelect)
118
+ }
119
+ }, [api, onSelect])
120
+
121
+ return (
122
+ <CarouselContext.Provider
123
+ value={{
124
+ carouselRef,
125
+ api: api,
126
+ opts,
127
+ orientation:
128
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
129
+ scrollPrev,
130
+ scrollNext,
131
+ canScrollPrev,
132
+ canScrollNext,
133
+ }}
134
+ >
135
+ <div
136
+ ref={ref}
137
+ onKeyDownCapture={handleKeyDown}
138
+ className={cn("relative", className)}
139
+ role="region"
140
+ aria-roledescription="carousel"
141
+ {...props}
142
+ >
143
+ {children}
144
+ </div>
145
+ </CarouselContext.Provider>
146
+ )
147
+ }
148
+ )
149
+ Carousel.displayName = "Carousel"
150
+
151
+ const CarouselContent = React.forwardRef<
152
+ HTMLDivElement,
153
+ React.HTMLAttributes<HTMLDivElement>
154
+ >(({ className, ...props }, ref) => {
155
+ const { carouselRef, orientation } = useCarousel()
156
+
157
+ return (
158
+ <div ref={carouselRef} className="overflow-hidden">
159
+ <div
160
+ ref={ref}
161
+ className={cn(
162
+ "flex",
163
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
164
+ className
165
+ )}
166
+ {...props}
167
+ />
168
+ </div>
169
+ )
170
+ })
171
+ CarouselContent.displayName = "CarouselContent"
172
+
173
+ const CarouselItem = React.forwardRef<
174
+ HTMLDivElement,
175
+ React.HTMLAttributes<HTMLDivElement>
176
+ >(({ className, ...props }, ref) => {
177
+ const { orientation } = useCarousel()
178
+
179
+ return (
180
+ <div
181
+ ref={ref}
182
+ role="group"
183
+ aria-roledescription="slide"
184
+ className={cn(
185
+ "min-w-0 shrink-0 grow-0 basis-full",
186
+ orientation === "horizontal" ? "pl-4" : "pt-4",
187
+ className
188
+ )}
189
+ {...props}
190
+ />
191
+ )
192
+ })
193
+ CarouselItem.displayName = "CarouselItem"
194
+
195
+ const CarouselPrevious = React.forwardRef<
196
+ HTMLButtonElement,
197
+ React.ComponentProps<typeof Button>
198
+ >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
199
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
200
+
201
+ return (
202
+ <Button
203
+ ref={ref}
204
+ variant={variant}
205
+ size={size}
206
+ className={cn(
207
+ "absolute h-8 w-8 rounded-full",
208
+ orientation === "horizontal"
209
+ ? "-left-12 top-1/2 -translate-y-1/2"
210
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
211
+ className
212
+ )}
213
+ disabled={!canScrollPrev}
214
+ onClick={scrollPrev}
215
+ {...props}
216
+ >
217
+ <ArrowLeft className="h-4 w-4" />
218
+ <span className="sr-only">Previous slide</span>
219
+ </Button>
220
+ )
221
+ })
222
+ CarouselPrevious.displayName = "CarouselPrevious"
223
+
224
+ const CarouselNext = React.forwardRef<
225
+ HTMLButtonElement,
226
+ React.ComponentProps<typeof Button>
227
+ >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
228
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
229
+
230
+ return (
231
+ <Button
232
+ ref={ref}
233
+ variant={variant}
234
+ size={size}
235
+ className={cn(
236
+ "absolute h-8 w-8 rounded-full",
237
+ orientation === "horizontal"
238
+ ? "-right-12 top-1/2 -translate-y-1/2"
239
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
240
+ className
241
+ )}
242
+ disabled={!canScrollNext}
243
+ onClick={scrollNext}
244
+ {...props}
245
+ >
246
+ <ArrowRight className="h-4 w-4" />
247
+ <span className="sr-only">Next slide</span>
248
+ </Button>
249
+ )
250
+ })
251
+ CarouselNext.displayName = "CarouselNext"
252
+
253
+ export {
254
+ type CarouselApi,
255
+ Carousel,
256
+ CarouselContent,
257
+ CarouselItem,
258
+ CarouselPrevious,
259
+ CarouselNext,
260
+ }
client/src/components/ui/chart.tsx ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RechartsPrimitive from "recharts"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ // Format: { THEME_NAME: CSS_SELECTOR }
9
+ const THEMES = { light: "", dark: ".dark" } as const
10
+
11
+ export type ChartConfig = {
12
+ [k in string]: {
13
+ label?: React.ReactNode
14
+ icon?: React.ComponentType
15
+ } & (
16
+ | { color?: string; theme?: never }
17
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
18
+ )
19
+ }
20
+
21
+ type ChartContextProps = {
22
+ config: ChartConfig
23
+ }
24
+
25
+ const ChartContext = React.createContext<ChartContextProps | null>(null)
26
+
27
+ function useChart() {
28
+ const context = React.useContext(ChartContext)
29
+
30
+ if (!context) {
31
+ throw new Error("useChart must be used within a <ChartContainer />")
32
+ }
33
+
34
+ return context
35
+ }
36
+
37
+ const ChartContainer = React.forwardRef<
38
+ HTMLDivElement,
39
+ React.ComponentProps<"div"> & {
40
+ config: ChartConfig
41
+ children: React.ComponentProps<
42
+ typeof RechartsPrimitive.ResponsiveContainer
43
+ >["children"]
44
+ }
45
+ >(({ id, className, children, config, ...props }, ref) => {
46
+ const uniqueId = React.useId()
47
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
48
+
49
+ return (
50
+ <ChartContext.Provider value={{ config }}>
51
+ <div
52
+ data-chart={chartId}
53
+ ref={ref}
54
+ className={cn(
55
+ "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
56
+ className
57
+ )}
58
+ {...props}
59
+ >
60
+ <ChartStyle id={chartId} config={config} />
61
+ <RechartsPrimitive.ResponsiveContainer>
62
+ {children}
63
+ </RechartsPrimitive.ResponsiveContainer>
64
+ </div>
65
+ </ChartContext.Provider>
66
+ )
67
+ })
68
+ ChartContainer.displayName = "Chart"
69
+
70
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
71
+ const colorConfig = Object.entries(config).filter(
72
+ ([, config]) => config.theme || config.color
73
+ )
74
+
75
+ if (!colorConfig.length) {
76
+ return null
77
+ }
78
+
79
+ return (
80
+ <style
81
+ dangerouslySetInnerHTML={{
82
+ __html: Object.entries(THEMES)
83
+ .map(
84
+ ([theme, prefix]) => `
85
+ ${prefix} [data-chart=${id}] {
86
+ ${colorConfig
87
+ .map(([key, itemConfig]) => {
88
+ const color =
89
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
90
+ itemConfig.color
91
+ return color ? ` --color-${key}: ${color};` : null
92
+ })
93
+ .join("\n")}
94
+ }
95
+ `
96
+ )
97
+ .join("\n"),
98
+ }}
99
+ />
100
+ )
101
+ }
102
+
103
+ const ChartTooltip = RechartsPrimitive.Tooltip
104
+
105
+ const ChartTooltipContent = React.forwardRef<
106
+ HTMLDivElement,
107
+ React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
108
+ React.ComponentProps<"div"> & {
109
+ hideLabel?: boolean
110
+ hideIndicator?: boolean
111
+ indicator?: "line" | "dot" | "dashed"
112
+ nameKey?: string
113
+ labelKey?: string
114
+ }
115
+ >(
116
+ (
117
+ {
118
+ active,
119
+ payload,
120
+ className,
121
+ indicator = "dot",
122
+ hideLabel = false,
123
+ hideIndicator = false,
124
+ label,
125
+ labelFormatter,
126
+ labelClassName,
127
+ formatter,
128
+ color,
129
+ nameKey,
130
+ labelKey,
131
+ },
132
+ ref
133
+ ) => {
134
+ const { config } = useChart()
135
+
136
+ const tooltipLabel = React.useMemo(() => {
137
+ if (hideLabel || !payload?.length) {
138
+ return null
139
+ }
140
+
141
+ const [item] = payload
142
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`
143
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
144
+ const value =
145
+ !labelKey && typeof label === "string"
146
+ ? config[label as keyof typeof config]?.label || label
147
+ : itemConfig?.label
148
+
149
+ if (labelFormatter) {
150
+ return (
151
+ <div className={cn("font-medium", labelClassName)}>
152
+ {labelFormatter(value, payload)}
153
+ </div>
154
+ )
155
+ }
156
+
157
+ if (!value) {
158
+ return null
159
+ }
160
+
161
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>
162
+ }, [
163
+ label,
164
+ labelFormatter,
165
+ payload,
166
+ hideLabel,
167
+ labelClassName,
168
+ config,
169
+ labelKey,
170
+ ])
171
+
172
+ if (!active || !payload?.length) {
173
+ return null
174
+ }
175
+
176
+ const nestLabel = payload.length === 1 && indicator !== "dot"
177
+
178
+ return (
179
+ <div
180
+ ref={ref}
181
+ className={cn(
182
+ "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
183
+ className
184
+ )}
185
+ >
186
+ {!nestLabel ? tooltipLabel : null}
187
+ <div className="grid gap-1.5">
188
+ {payload.map((item, index) => {
189
+ const key = `${nameKey || item.name || item.dataKey || "value"}`
190
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
191
+ const indicatorColor = color || item.payload.fill || item.color
192
+
193
+ return (
194
+ <div
195
+ key={item.dataKey}
196
+ className={cn(
197
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
198
+ indicator === "dot" && "items-center"
199
+ )}
200
+ >
201
+ {formatter && item?.value !== undefined && item.name ? (
202
+ formatter(item.value, item.name, item, index, item.payload)
203
+ ) : (
204
+ <>
205
+ {itemConfig?.icon ? (
206
+ <itemConfig.icon />
207
+ ) : (
208
+ !hideIndicator && (
209
+ <div
210
+ className={cn(
211
+ "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
212
+ {
213
+ "h-2.5 w-2.5": indicator === "dot",
214
+ "w-1": indicator === "line",
215
+ "w-0 border-[1.5px] border-dashed bg-transparent":
216
+ indicator === "dashed",
217
+ "my-0.5": nestLabel && indicator === "dashed",
218
+ }
219
+ )}
220
+ style={
221
+ {
222
+ "--color-bg": indicatorColor,
223
+ "--color-border": indicatorColor,
224
+ } as React.CSSProperties
225
+ }
226
+ />
227
+ )
228
+ )}
229
+ <div
230
+ className={cn(
231
+ "flex flex-1 justify-between leading-none",
232
+ nestLabel ? "items-end" : "items-center"
233
+ )}
234
+ >
235
+ <div className="grid gap-1.5">
236
+ {nestLabel ? tooltipLabel : null}
237
+ <span className="text-muted-foreground">
238
+ {itemConfig?.label || item.name}
239
+ </span>
240
+ </div>
241
+ {item.value && (
242
+ <span className="font-mono font-medium tabular-nums text-foreground">
243
+ {item.value.toLocaleString()}
244
+ </span>
245
+ )}
246
+ </div>
247
+ </>
248
+ )}
249
+ </div>
250
+ )
251
+ })}
252
+ </div>
253
+ </div>
254
+ )
255
+ }
256
+ )
257
+ ChartTooltipContent.displayName = "ChartTooltip"
258
+
259
+ const ChartLegend = RechartsPrimitive.Legend
260
+
261
+ const ChartLegendContent = React.forwardRef<
262
+ HTMLDivElement,
263
+ React.ComponentProps<"div"> &
264
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
265
+ hideIcon?: boolean
266
+ nameKey?: string
267
+ }
268
+ >(
269
+ (
270
+ { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
271
+ ref
272
+ ) => {
273
+ const { config } = useChart()
274
+
275
+ if (!payload?.length) {
276
+ return null
277
+ }
278
+
279
+ return (
280
+ <div
281
+ ref={ref}
282
+ className={cn(
283
+ "flex items-center justify-center gap-4",
284
+ verticalAlign === "top" ? "pb-3" : "pt-3",
285
+ className
286
+ )}
287
+ >
288
+ {payload.map((item) => {
289
+ const key = `${nameKey || item.dataKey || "value"}`
290
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
291
+
292
+ return (
293
+ <div
294
+ key={item.value}
295
+ className={cn(
296
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
297
+ )}
298
+ >
299
+ {itemConfig?.icon && !hideIcon ? (
300
+ <itemConfig.icon />
301
+ ) : (
302
+ <div
303
+ className="h-2 w-2 shrink-0 rounded-[2px]"
304
+ style={{
305
+ backgroundColor: item.color,
306
+ }}
307
+ />
308
+ )}
309
+ {itemConfig?.label}
310
+ </div>
311
+ )
312
+ })}
313
+ </div>
314
+ )
315
+ }
316
+ )
317
+ ChartLegendContent.displayName = "ChartLegend"
318
+
319
+ // Helper to extract item config from a payload.
320
+ function getPayloadConfigFromPayload(
321
+ config: ChartConfig,
322
+ payload: unknown,
323
+ key: string
324
+ ) {
325
+ if (typeof payload !== "object" || payload === null) {
326
+ return undefined
327
+ }
328
+
329
+ const payloadPayload =
330
+ "payload" in payload &&
331
+ typeof payload.payload === "object" &&
332
+ payload.payload !== null
333
+ ? payload.payload
334
+ : undefined
335
+
336
+ let configLabelKey: string = key
337
+
338
+ if (
339
+ key in payload &&
340
+ typeof payload[key as keyof typeof payload] === "string"
341
+ ) {
342
+ configLabelKey = payload[key as keyof typeof payload] as string
343
+ } else if (
344
+ payloadPayload &&
345
+ key in payloadPayload &&
346
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
347
+ ) {
348
+ configLabelKey = payloadPayload[
349
+ key as keyof typeof payloadPayload
350
+ ] as string
351
+ }
352
+
353
+ return configLabelKey in config
354
+ ? config[configLabelKey]
355
+ : config[key as keyof typeof config]
356
+ }
357
+
358
+ export {
359
+ ChartContainer,
360
+ ChartTooltip,
361
+ ChartTooltipContent,
362
+ ChartLegend,
363
+ ChartLegendContent,
364
+ ChartStyle,
365
+ }
client/src/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3
+ import { Check } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
15
+ className
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator
20
+ className={cn("flex items-center justify-center text-current")}
21
+ >
22
+ <Check className="h-4 w-4" />
23
+ </CheckboxPrimitive.Indicator>
24
+ </CheckboxPrimitive.Root>
25
+ ))
26
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
27
+
28
+ export { Checkbox }
client/src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4
+
5
+ const Collapsible = CollapsiblePrimitive.Root
6
+
7
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8
+
9
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10
+
11
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
client/src/components/ui/command.tsx ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { type DialogProps } from "@radix-ui/react-dialog"
3
+ import { Command as CommandPrimitive } from "cmdk"
4
+ import { Search } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Dialog, DialogContent } from "@/components/ui/dialog"
8
+
9
+ const Command = React.forwardRef<
10
+ React.ElementRef<typeof CommandPrimitive>,
11
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
12
+ >(({ className, ...props }, ref) => (
13
+ <CommandPrimitive
14
+ ref={ref}
15
+ className={cn(
16
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ ))
22
+ Command.displayName = CommandPrimitive.displayName
23
+
24
+ const CommandDialog = ({ children, ...props }: DialogProps) => {
25
+ return (
26
+ <Dialog {...props}>
27
+ <DialogContent className="overflow-hidden p-0 shadow-lg">
28
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
29
+ {children}
30
+ </Command>
31
+ </DialogContent>
32
+ </Dialog>
33
+ )
34
+ }
35
+
36
+ const CommandInput = React.forwardRef<
37
+ React.ElementRef<typeof CommandPrimitive.Input>,
38
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
39
+ >(({ className, ...props }, ref) => (
40
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
41
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
42
+ <CommandPrimitive.Input
43
+ ref={ref}
44
+ className={cn(
45
+ "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ </div>
51
+ ))
52
+
53
+ CommandInput.displayName = CommandPrimitive.Input.displayName
54
+
55
+ const CommandList = React.forwardRef<
56
+ React.ElementRef<typeof CommandPrimitive.List>,
57
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
58
+ >(({ className, ...props }, ref) => (
59
+ <CommandPrimitive.List
60
+ ref={ref}
61
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
62
+ {...props}
63
+ />
64
+ ))
65
+
66
+ CommandList.displayName = CommandPrimitive.List.displayName
67
+
68
+ const CommandEmpty = React.forwardRef<
69
+ React.ElementRef<typeof CommandPrimitive.Empty>,
70
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
71
+ >((props, ref) => (
72
+ <CommandPrimitive.Empty
73
+ ref={ref}
74
+ className="py-6 text-center text-sm"
75
+ {...props}
76
+ />
77
+ ))
78
+
79
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName
80
+
81
+ const CommandGroup = React.forwardRef<
82
+ React.ElementRef<typeof CommandPrimitive.Group>,
83
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
84
+ >(({ className, ...props }, ref) => (
85
+ <CommandPrimitive.Group
86
+ ref={ref}
87
+ className={cn(
88
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
89
+ className
90
+ )}
91
+ {...props}
92
+ />
93
+ ))
94
+
95
+ CommandGroup.displayName = CommandPrimitive.Group.displayName
96
+
97
+ const CommandSeparator = React.forwardRef<
98
+ React.ElementRef<typeof CommandPrimitive.Separator>,
99
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
100
+ >(({ className, ...props }, ref) => (
101
+ <CommandPrimitive.Separator
102
+ ref={ref}
103
+ className={cn("-mx-1 h-px bg-border", className)}
104
+ {...props}
105
+ />
106
+ ))
107
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName
108
+
109
+ const CommandItem = React.forwardRef<
110
+ React.ElementRef<typeof CommandPrimitive.Item>,
111
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
112
+ >(({ className, ...props }, ref) => (
113
+ <CommandPrimitive.Item
114
+ ref={ref}
115
+ className={cn(
116
+ "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
117
+ className
118
+ )}
119
+ {...props}
120
+ />
121
+ ))
122
+
123
+ CommandItem.displayName = CommandPrimitive.Item.displayName
124
+
125
+ const CommandShortcut = ({
126
+ className,
127
+ ...props
128
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
129
+ return (
130
+ <span
131
+ className={cn(
132
+ "ml-auto text-xs tracking-widest text-muted-foreground",
133
+ className
134
+ )}
135
+ {...props}
136
+ />
137
+ )
138
+ }
139
+ CommandShortcut.displayName = "CommandShortcut"
140
+
141
+ export {
142
+ Command,
143
+ CommandDialog,
144
+ CommandInput,
145
+ CommandList,
146
+ CommandEmpty,
147
+ CommandGroup,
148
+ CommandItem,
149
+ CommandShortcut,
150
+ CommandSeparator,
151
+ }
client/src/components/ui/context-menu.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
3
+ import { Check, ChevronRight, Circle } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const ContextMenu = ContextMenuPrimitive.Root
8
+
9
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger
10
+
11
+ const ContextMenuGroup = ContextMenuPrimitive.Group
12
+
13
+ const ContextMenuPortal = ContextMenuPrimitive.Portal
14
+
15
+ const ContextMenuSub = ContextMenuPrimitive.Sub
16
+
17
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
18
+
19
+ const ContextMenuSubTrigger = React.forwardRef<
20
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
21
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean
23
+ }
24
+ >(({ className, inset, children, ...props }, ref) => (
25
+ <ContextMenuPrimitive.SubTrigger
26
+ ref={ref}
27
+ className={cn(
28
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
29
+ inset && "pl-8",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto h-4 w-4" />
36
+ </ContextMenuPrimitive.SubTrigger>
37
+ ))
38
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
39
+
40
+ const ContextMenuSubContent = React.forwardRef<
41
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
42
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
43
+ >(({ className, ...props }, ref) => (
44
+ <ContextMenuPrimitive.SubContent
45
+ ref={ref}
46
+ className={cn(
47
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ ))
53
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
54
+
55
+ const ContextMenuContent = React.forwardRef<
56
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
57
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
58
+ >(({ className, ...props }, ref) => (
59
+ <ContextMenuPrimitive.Portal>
60
+ <ContextMenuPrimitive.Content
61
+ ref={ref}
62
+ className={cn(
63
+ "z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ </ContextMenuPrimitive.Portal>
69
+ ))
70
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
71
+
72
+ const ContextMenuItem = React.forwardRef<
73
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
74
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
75
+ inset?: boolean
76
+ }
77
+ >(({ className, inset, ...props }, ref) => (
78
+ <ContextMenuPrimitive.Item
79
+ ref={ref}
80
+ className={cn(
81
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
82
+ inset && "pl-8",
83
+ className
84
+ )}
85
+ {...props}
86
+ />
87
+ ))
88
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
89
+
90
+ const ContextMenuCheckboxItem = React.forwardRef<
91
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
92
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
93
+ >(({ className, children, checked, ...props }, ref) => (
94
+ <ContextMenuPrimitive.CheckboxItem
95
+ ref={ref}
96
+ className={cn(
97
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
98
+ className
99
+ )}
100
+ checked={checked}
101
+ {...props}
102
+ >
103
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
104
+ <ContextMenuPrimitive.ItemIndicator>
105
+ <Check className="h-4 w-4" />
106
+ </ContextMenuPrimitive.ItemIndicator>
107
+ </span>
108
+ {children}
109
+ </ContextMenuPrimitive.CheckboxItem>
110
+ ))
111
+ ContextMenuCheckboxItem.displayName =
112
+ ContextMenuPrimitive.CheckboxItem.displayName
113
+
114
+ const ContextMenuRadioItem = React.forwardRef<
115
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
116
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
117
+ >(({ className, children, ...props }, ref) => (
118
+ <ContextMenuPrimitive.RadioItem
119
+ ref={ref}
120
+ className={cn(
121
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
122
+ className
123
+ )}
124
+ {...props}
125
+ >
126
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
127
+ <ContextMenuPrimitive.ItemIndicator>
128
+ <Circle className="h-2 w-2 fill-current" />
129
+ </ContextMenuPrimitive.ItemIndicator>
130
+ </span>
131
+ {children}
132
+ </ContextMenuPrimitive.RadioItem>
133
+ ))
134
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
135
+
136
+ const ContextMenuLabel = React.forwardRef<
137
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
138
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
139
+ inset?: boolean
140
+ }
141
+ >(({ className, inset, ...props }, ref) => (
142
+ <ContextMenuPrimitive.Label
143
+ ref={ref}
144
+ className={cn(
145
+ "px-2 py-1.5 text-sm font-semibold text-foreground",
146
+ inset && "pl-8",
147
+ className
148
+ )}
149
+ {...props}
150
+ />
151
+ ))
152
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
153
+
154
+ const ContextMenuSeparator = React.forwardRef<
155
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
156
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
157
+ >(({ className, ...props }, ref) => (
158
+ <ContextMenuPrimitive.Separator
159
+ ref={ref}
160
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
161
+ {...props}
162
+ />
163
+ ))
164
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
165
+
166
+ const ContextMenuShortcut = ({
167
+ className,
168
+ ...props
169
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
170
+ return (
171
+ <span
172
+ className={cn(
173
+ "ml-auto text-xs tracking-widest text-muted-foreground",
174
+ className
175
+ )}
176
+ {...props}
177
+ />
178
+ )
179
+ }
180
+ ContextMenuShortcut.displayName = "ContextMenuShortcut"
181
+
182
+ export {
183
+ ContextMenu,
184
+ ContextMenuTrigger,
185
+ ContextMenuContent,
186
+ ContextMenuItem,
187
+ ContextMenuCheckboxItem,
188
+ ContextMenuRadioItem,
189
+ ContextMenuLabel,
190
+ ContextMenuSeparator,
191
+ ContextMenuShortcut,
192
+ ContextMenuGroup,
193
+ ContextMenuPortal,
194
+ ContextMenuSub,
195
+ ContextMenuSubContent,
196
+ ContextMenuSubTrigger,
197
+ ContextMenuRadioGroup,
198
+ }
client/src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { X } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Dialog = DialogPrimitive.Root
10
+
11
+ const DialogTrigger = DialogPrimitive.Trigger
12
+
13
+ const DialogPortal = DialogPrimitive.Portal
14
+
15
+ const DialogClose = DialogPrimitive.Close
16
+
17
+ const DialogOverlay = React.forwardRef<
18
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
19
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20
+ >(({ className, ...props }, ref) => (
21
+ <DialogPrimitive.Overlay
22
+ ref={ref}
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ ))
30
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31
+
32
+ const DialogContent = React.forwardRef<
33
+ React.ElementRef<typeof DialogPrimitive.Content>,
34
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
+ >(({ className, children, ...props }, ref) => (
36
+ <DialogPortal>
37
+ <DialogOverlay />
38
+ <DialogPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
42
+ className
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
48
+ <X className="h-4 w-4" />
49
+ <span className="sr-only">Close</span>
50
+ </DialogPrimitive.Close>
51
+ </DialogPrimitive.Content>
52
+ </DialogPortal>
53
+ ))
54
+ DialogContent.displayName = DialogPrimitive.Content.displayName
55
+
56
+ const DialogHeader = ({
57
+ className,
58
+ ...props
59
+ }: React.HTMLAttributes<HTMLDivElement>) => (
60
+ <div
61
+ className={cn(
62
+ "flex flex-col space-y-1.5 text-center sm:text-left",
63
+ className
64
+ )}
65
+ {...props}
66
+ />
67
+ )
68
+ DialogHeader.displayName = "DialogHeader"
69
+
70
+ const DialogFooter = ({
71
+ className,
72
+ ...props
73
+ }: React.HTMLAttributes<HTMLDivElement>) => (
74
+ <div
75
+ className={cn(
76
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ )
82
+ DialogFooter.displayName = "DialogFooter"
83
+
84
+ const DialogTitle = React.forwardRef<
85
+ React.ElementRef<typeof DialogPrimitive.Title>,
86
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
87
+ >(({ className, ...props }, ref) => (
88
+ <DialogPrimitive.Title
89
+ ref={ref}
90
+ className={cn(
91
+ "text-lg font-semibold leading-none tracking-tight",
92
+ className
93
+ )}
94
+ {...props}
95
+ />
96
+ ))
97
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
98
+
99
+ const DialogDescription = React.forwardRef<
100
+ React.ElementRef<typeof DialogPrimitive.Description>,
101
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
102
+ >(({ className, ...props }, ref) => (
103
+ <DialogPrimitive.Description
104
+ ref={ref}
105
+ className={cn("text-sm text-muted-foreground", className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ DialogDescription.displayName = DialogPrimitive.Description.displayName
110
+
111
+ export {
112
+ Dialog,
113
+ DialogPortal,
114
+ DialogOverlay,
115
+ DialogClose,
116
+ DialogTrigger,
117
+ DialogContent,
118
+ DialogHeader,
119
+ DialogFooter,
120
+ DialogTitle,
121
+ DialogDescription,
122
+ }
client/src/components/ui/drawer.tsx ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Drawer as DrawerPrimitive } from "vaul"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Drawer = ({
9
+ shouldScaleBackground = true,
10
+ ...props
11
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
12
+ <DrawerPrimitive.Root
13
+ shouldScaleBackground={shouldScaleBackground}
14
+ {...props}
15
+ />
16
+ )
17
+ Drawer.displayName = "Drawer"
18
+
19
+ const DrawerTrigger = DrawerPrimitive.Trigger
20
+
21
+ const DrawerPortal = DrawerPrimitive.Portal
22
+
23
+ const DrawerClose = DrawerPrimitive.Close
24
+
25
+ const DrawerOverlay = React.forwardRef<
26
+ React.ElementRef<typeof DrawerPrimitive.Overlay>,
27
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
28
+ >(({ className, ...props }, ref) => (
29
+ <DrawerPrimitive.Overlay
30
+ ref={ref}
31
+ className={cn("fixed inset-0 z-50 bg-black/80", className)}
32
+ {...props}
33
+ />
34
+ ))
35
+ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
36
+
37
+ const DrawerContent = React.forwardRef<
38
+ React.ElementRef<typeof DrawerPrimitive.Content>,
39
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
40
+ >(({ className, children, ...props }, ref) => (
41
+ <DrawerPortal>
42
+ <DrawerOverlay />
43
+ <DrawerPrimitive.Content
44
+ ref={ref}
45
+ className={cn(
46
+ "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
47
+ className
48
+ )}
49
+ {...props}
50
+ >
51
+ <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
52
+ {children}
53
+ </DrawerPrimitive.Content>
54
+ </DrawerPortal>
55
+ ))
56
+ DrawerContent.displayName = "DrawerContent"
57
+
58
+ const DrawerHeader = ({
59
+ className,
60
+ ...props
61
+ }: React.HTMLAttributes<HTMLDivElement>) => (
62
+ <div
63
+ className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
64
+ {...props}
65
+ />
66
+ )
67
+ DrawerHeader.displayName = "DrawerHeader"
68
+
69
+ const DrawerFooter = ({
70
+ className,
71
+ ...props
72
+ }: React.HTMLAttributes<HTMLDivElement>) => (
73
+ <div
74
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
75
+ {...props}
76
+ />
77
+ )
78
+ DrawerFooter.displayName = "DrawerFooter"
79
+
80
+ const DrawerTitle = React.forwardRef<
81
+ React.ElementRef<typeof DrawerPrimitive.Title>,
82
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
83
+ >(({ className, ...props }, ref) => (
84
+ <DrawerPrimitive.Title
85
+ ref={ref}
86
+ className={cn(
87
+ "text-lg font-semibold leading-none tracking-tight",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ ))
93
+ DrawerTitle.displayName = DrawerPrimitive.Title.displayName
94
+
95
+ const DrawerDescription = React.forwardRef<
96
+ React.ElementRef<typeof DrawerPrimitive.Description>,
97
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
98
+ >(({ className, ...props }, ref) => (
99
+ <DrawerPrimitive.Description
100
+ ref={ref}
101
+ className={cn("text-sm text-muted-foreground", className)}
102
+ {...props}
103
+ />
104
+ ))
105
+ DrawerDescription.displayName = DrawerPrimitive.Description.displayName
106
+
107
+ export {
108
+ Drawer,
109
+ DrawerPortal,
110
+ DrawerOverlay,
111
+ DrawerTrigger,
112
+ DrawerClose,
113
+ DrawerContent,
114
+ DrawerHeader,
115
+ DrawerFooter,
116
+ DrawerTitle,
117
+ DrawerDescription,
118
+ }
client/src/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3
+ import { Check, ChevronRight, Circle } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const DropdownMenu = DropdownMenuPrimitive.Root
8
+
9
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
10
+
11
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group
12
+
13
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
14
+
15
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub
16
+
17
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
18
+
19
+ const DropdownMenuSubTrigger = React.forwardRef<
20
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
21
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean
23
+ }
24
+ >(({ className, inset, children, ...props }, ref) => (
25
+ <DropdownMenuPrimitive.SubTrigger
26
+ ref={ref}
27
+ className={cn(
28
+ "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
29
+ inset && "pl-8",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto" />
36
+ </DropdownMenuPrimitive.SubTrigger>
37
+ ))
38
+ DropdownMenuSubTrigger.displayName =
39
+ DropdownMenuPrimitive.SubTrigger.displayName
40
+
41
+ const DropdownMenuSubContent = React.forwardRef<
42
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
43
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
44
+ >(({ className, ...props }, ref) => (
45
+ <DropdownMenuPrimitive.SubContent
46
+ ref={ref}
47
+ className={cn(
48
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
49
+ className
50
+ )}
51
+ {...props}
52
+ />
53
+ ))
54
+ DropdownMenuSubContent.displayName =
55
+ DropdownMenuPrimitive.SubContent.displayName
56
+
57
+ const DropdownMenuContent = React.forwardRef<
58
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
59
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
60
+ >(({ className, sideOffset = 4, ...props }, ref) => (
61
+ <DropdownMenuPrimitive.Portal>
62
+ <DropdownMenuPrimitive.Content
63
+ ref={ref}
64
+ sideOffset={sideOffset}
65
+ className={cn(
66
+ "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
67
+ className
68
+ )}
69
+ {...props}
70
+ />
71
+ </DropdownMenuPrimitive.Portal>
72
+ ))
73
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
74
+
75
+ const DropdownMenuItem = React.forwardRef<
76
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
77
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
78
+ inset?: boolean
79
+ }
80
+ >(({ className, inset, ...props }, ref) => (
81
+ <DropdownMenuPrimitive.Item
82
+ ref={ref}
83
+ className={cn(
84
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
85
+ inset && "pl-8",
86
+ className
87
+ )}
88
+ {...props}
89
+ />
90
+ ))
91
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
92
+
93
+ const DropdownMenuCheckboxItem = React.forwardRef<
94
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
95
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
96
+ >(({ className, children, checked, ...props }, ref) => (
97
+ <DropdownMenuPrimitive.CheckboxItem
98
+ ref={ref}
99
+ className={cn(
100
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
101
+ className
102
+ )}
103
+ checked={checked}
104
+ {...props}
105
+ >
106
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
107
+ <DropdownMenuPrimitive.ItemIndicator>
108
+ <Check className="h-4 w-4" />
109
+ </DropdownMenuPrimitive.ItemIndicator>
110
+ </span>
111
+ {children}
112
+ </DropdownMenuPrimitive.CheckboxItem>
113
+ ))
114
+ DropdownMenuCheckboxItem.displayName =
115
+ DropdownMenuPrimitive.CheckboxItem.displayName
116
+
117
+ const DropdownMenuRadioItem = React.forwardRef<
118
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
119
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
120
+ >(({ className, children, ...props }, ref) => (
121
+ <DropdownMenuPrimitive.RadioItem
122
+ ref={ref}
123
+ className={cn(
124
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
125
+ className
126
+ )}
127
+ {...props}
128
+ >
129
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
130
+ <DropdownMenuPrimitive.ItemIndicator>
131
+ <Circle className="h-2 w-2 fill-current" />
132
+ </DropdownMenuPrimitive.ItemIndicator>
133
+ </span>
134
+ {children}
135
+ </DropdownMenuPrimitive.RadioItem>
136
+ ))
137
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
138
+
139
+ const DropdownMenuLabel = React.forwardRef<
140
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
141
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
142
+ inset?: boolean
143
+ }
144
+ >(({ className, inset, ...props }, ref) => (
145
+ <DropdownMenuPrimitive.Label
146
+ ref={ref}
147
+ className={cn(
148
+ "px-2 py-1.5 text-sm font-semibold",
149
+ inset && "pl-8",
150
+ className
151
+ )}
152
+ {...props}
153
+ />
154
+ ))
155
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
156
+
157
+ const DropdownMenuSeparator = React.forwardRef<
158
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
159
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
160
+ >(({ className, ...props }, ref) => (
161
+ <DropdownMenuPrimitive.Separator
162
+ ref={ref}
163
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
164
+ {...props}
165
+ />
166
+ ))
167
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
168
+
169
+ const DropdownMenuShortcut = ({
170
+ className,
171
+ ...props
172
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
173
+ return (
174
+ <span
175
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
176
+ {...props}
177
+ />
178
+ )
179
+ }
180
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
181
+
182
+ export {
183
+ DropdownMenu,
184
+ DropdownMenuTrigger,
185
+ DropdownMenuContent,
186
+ DropdownMenuItem,
187
+ DropdownMenuCheckboxItem,
188
+ DropdownMenuRadioItem,
189
+ DropdownMenuLabel,
190
+ DropdownMenuSeparator,
191
+ DropdownMenuShortcut,
192
+ DropdownMenuGroup,
193
+ DropdownMenuPortal,
194
+ DropdownMenuSub,
195
+ DropdownMenuSubContent,
196
+ DropdownMenuSubTrigger,
197
+ DropdownMenuRadioGroup,
198
+ }
client/src/components/ui/form.tsx ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as LabelPrimitive from "@radix-ui/react-label"
5
+ import { Slot } from "@radix-ui/react-slot"
6
+ import {
7
+ Controller,
8
+ FormProvider,
9
+ useFormContext,
10
+ type ControllerProps,
11
+ type FieldPath,
12
+ type FieldValues,
13
+ } from "react-hook-form"
14
+
15
+ import { cn } from "@/lib/utils"
16
+ import { Label } from "@/components/ui/label"
17
+
18
+ const Form = FormProvider
19
+
20
+ type FormFieldContextValue<
21
+ TFieldValues extends FieldValues = FieldValues,
22
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
23
+ > = {
24
+ name: TName
25
+ }
26
+
27
+ const FormFieldContext = React.createContext<FormFieldContextValue>(
28
+ {} as FormFieldContextValue
29
+ )
30
+
31
+ const FormField = <
32
+ TFieldValues extends FieldValues = FieldValues,
33
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
34
+ >({
35
+ ...props
36
+ }: ControllerProps<TFieldValues, TName>) => {
37
+ return (
38
+ <FormFieldContext.Provider value={{ name: props.name }}>
39
+ <Controller {...props} />
40
+ </FormFieldContext.Provider>
41
+ )
42
+ }
43
+
44
+ const useFormField = () => {
45
+ const fieldContext = React.useContext(FormFieldContext)
46
+ const itemContext = React.useContext(FormItemContext)
47
+ const { getFieldState, formState } = useFormContext()
48
+
49
+ const fieldState = getFieldState(fieldContext.name, formState)
50
+
51
+ if (!fieldContext) {
52
+ throw new Error("useFormField should be used within <FormField>")
53
+ }
54
+
55
+ const { id } = itemContext
56
+
57
+ return {
58
+ id,
59
+ name: fieldContext.name,
60
+ formItemId: `${id}-form-item`,
61
+ formDescriptionId: `${id}-form-item-description`,
62
+ formMessageId: `${id}-form-item-message`,
63
+ ...fieldState,
64
+ }
65
+ }
66
+
67
+ type FormItemContextValue = {
68
+ id: string
69
+ }
70
+
71
+ const FormItemContext = React.createContext<FormItemContextValue>(
72
+ {} as FormItemContextValue
73
+ )
74
+
75
+ const FormItem = React.forwardRef<
76
+ HTMLDivElement,
77
+ React.HTMLAttributes<HTMLDivElement>
78
+ >(({ className, ...props }, ref) => {
79
+ const id = React.useId()
80
+
81
+ return (
82
+ <FormItemContext.Provider value={{ id }}>
83
+ <div ref={ref} className={cn("space-y-2", className)} {...props} />
84
+ </FormItemContext.Provider>
85
+ )
86
+ })
87
+ FormItem.displayName = "FormItem"
88
+
89
+ const FormLabel = React.forwardRef<
90
+ React.ElementRef<typeof LabelPrimitive.Root>,
91
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
92
+ >(({ className, ...props }, ref) => {
93
+ const { error, formItemId } = useFormField()
94
+
95
+ return (
96
+ <Label
97
+ ref={ref}
98
+ className={cn(error && "text-destructive", className)}
99
+ htmlFor={formItemId}
100
+ {...props}
101
+ />
102
+ )
103
+ })
104
+ FormLabel.displayName = "FormLabel"
105
+
106
+ const FormControl = React.forwardRef<
107
+ React.ElementRef<typeof Slot>,
108
+ React.ComponentPropsWithoutRef<typeof Slot>
109
+ >(({ ...props }, ref) => {
110
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111
+
112
+ return (
113
+ <Slot
114
+ ref={ref}
115
+ id={formItemId}
116
+ aria-describedby={
117
+ !error
118
+ ? `${formDescriptionId}`
119
+ : `${formDescriptionId} ${formMessageId}`
120
+ }
121
+ aria-invalid={!!error}
122
+ {...props}
123
+ />
124
+ )
125
+ })
126
+ FormControl.displayName = "FormControl"
127
+
128
+ const FormDescription = React.forwardRef<
129
+ HTMLParagraphElement,
130
+ React.HTMLAttributes<HTMLParagraphElement>
131
+ >(({ className, ...props }, ref) => {
132
+ const { formDescriptionId } = useFormField()
133
+
134
+ return (
135
+ <p
136
+ ref={ref}
137
+ id={formDescriptionId}
138
+ className={cn("text-sm text-muted-foreground", className)}
139
+ {...props}
140
+ />
141
+ )
142
+ })
143
+ FormDescription.displayName = "FormDescription"
144
+
145
+ const FormMessage = React.forwardRef<
146
+ HTMLParagraphElement,
147
+ React.HTMLAttributes<HTMLParagraphElement>
148
+ >(({ className, children, ...props }, ref) => {
149
+ const { error, formMessageId } = useFormField()
150
+ const body = error ? String(error?.message ?? "") : children
151
+
152
+ if (!body) {
153
+ return null
154
+ }
155
+
156
+ return (
157
+ <p
158
+ ref={ref}
159
+ id={formMessageId}
160
+ className={cn("text-sm font-medium text-destructive", className)}
161
+ {...props}
162
+ >
163
+ {body}
164
+ </p>
165
+ )
166
+ })
167
+ FormMessage.displayName = "FormMessage"
168
+
169
+ export {
170
+ useFormField,
171
+ Form,
172
+ FormItem,
173
+ FormLabel,
174
+ FormControl,
175
+ FormDescription,
176
+ FormMessage,
177
+ FormField,
178
+ }
client/src/components/ui/hover-card.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const HoverCard = HoverCardPrimitive.Root
9
+
10
+ const HoverCardTrigger = HoverCardPrimitive.Trigger
11
+
12
+ const HoverCardContent = React.forwardRef<
13
+ React.ElementRef<typeof HoverCardPrimitive.Content>,
14
+ React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
15
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16
+ <HoverCardPrimitive.Content
17
+ ref={ref}
18
+ align={align}
19
+ sideOffset={sideOffset}
20
+ className={cn(
21
+ "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
22
+ className
23
+ )}
24
+ {...props}
25
+ />
26
+ ))
27
+ HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28
+
29
+ export { HoverCard, HoverCardTrigger, HoverCardContent }
client/src/components/ui/input-otp.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { OTPInput, OTPInputContext } from "input-otp"
3
+ import { Dot } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const InputOTP = React.forwardRef<
8
+ React.ElementRef<typeof OTPInput>,
9
+ React.ComponentPropsWithoutRef<typeof OTPInput>
10
+ >(({ className, containerClassName, ...props }, ref) => (
11
+ <OTPInput
12
+ ref={ref}
13
+ containerClassName={cn(
14
+ "flex items-center gap-2 has-[:disabled]:opacity-50",
15
+ containerClassName
16
+ )}
17
+ className={cn("disabled:cursor-not-allowed", className)}
18
+ {...props}
19
+ />
20
+ ))
21
+ InputOTP.displayName = "InputOTP"
22
+
23
+ const InputOTPGroup = React.forwardRef<
24
+ React.ElementRef<"div">,
25
+ React.ComponentPropsWithoutRef<"div">
26
+ >(({ className, ...props }, ref) => (
27
+ <div ref={ref} className={cn("flex items-center", className)} {...props} />
28
+ ))
29
+ InputOTPGroup.displayName = "InputOTPGroup"
30
+
31
+ const InputOTPSlot = React.forwardRef<
32
+ React.ElementRef<"div">,
33
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
34
+ >(({ index, className, ...props }, ref) => {
35
+ const inputOTPContext = React.useContext(OTPInputContext)
36
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
37
+
38
+ return (
39
+ <div
40
+ ref={ref}
41
+ className={cn(
42
+ "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
43
+ isActive && "z-10 ring-2 ring-ring ring-offset-background",
44
+ className
45
+ )}
46
+ {...props}
47
+ >
48
+ {char}
49
+ {hasFakeCaret && (
50
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
51
+ <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
52
+ </div>
53
+ )}
54
+ </div>
55
+ )
56
+ })
57
+ InputOTPSlot.displayName = "InputOTPSlot"
58
+
59
+ const InputOTPSeparator = React.forwardRef<
60
+ React.ElementRef<"div">,
61
+ React.ComponentPropsWithoutRef<"div">
62
+ >(({ ...props }, ref) => (
63
+ <div ref={ref} role="separator" {...props}>
64
+ <Dot />
65
+ </div>
66
+ ))
67
+ InputOTPSeparator.displayName = "InputOTPSeparator"
68
+
69
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
client/src/components/ui/input.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export interface InputProps
6
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
7
+
8
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
+ ({ className, type, ...props }, ref) => {
10
+ return (
11
+ <input
12
+ type={type}
13
+ className={cn(
14
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
15
+ className
16
+ )}
17
+ ref={ref}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+ )
23
+ Input.displayName = "Input"
24
+
25
+ export { Input }
client/src/components/ui/label.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as LabelPrimitive from "@radix-ui/react-label"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const labelVariants = cva(
8
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9
+ )
10
+
11
+ const Label = React.forwardRef<
12
+ React.ElementRef<typeof LabelPrimitive.Root>,
13
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
14
+ VariantProps<typeof labelVariants>
15
+ >(({ className, ...props }, ref) => (
16
+ <LabelPrimitive.Root
17
+ ref={ref}
18
+ className={cn(labelVariants(), className)}
19
+ {...props}
20
+ />
21
+ ))
22
+ Label.displayName = LabelPrimitive.Root.displayName
23
+
24
+ export { Label }
client/src/components/ui/menubar.tsx ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as MenubarPrimitive from "@radix-ui/react-menubar"
5
+ import { Check, ChevronRight, Circle } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function MenubarMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
12
+ return <MenubarPrimitive.Menu {...props} />
13
+ }
14
+
15
+ function MenubarGroup({
16
+ ...props
17
+ }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
18
+ return <MenubarPrimitive.Group {...props} />
19
+ }
20
+
21
+ function MenubarPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
24
+ return <MenubarPrimitive.Portal {...props} />
25
+ }
26
+
27
+ function MenubarRadioGroup({
28
+ ...props
29
+ }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
30
+ return <MenubarPrimitive.RadioGroup {...props} />
31
+ }
32
+
33
+ function MenubarSub({
34
+ ...props
35
+ }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
36
+ return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
37
+ }
38
+
39
+ const Menubar = React.forwardRef<
40
+ React.ElementRef<typeof MenubarPrimitive.Root>,
41
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
42
+ >(({ className, ...props }, ref) => (
43
+ <MenubarPrimitive.Root
44
+ ref={ref}
45
+ className={cn(
46
+ "flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
47
+ className
48
+ )}
49
+ {...props}
50
+ />
51
+ ))
52
+ Menubar.displayName = MenubarPrimitive.Root.displayName
53
+
54
+ const MenubarTrigger = React.forwardRef<
55
+ React.ElementRef<typeof MenubarPrimitive.Trigger>,
56
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
57
+ >(({ className, ...props }, ref) => (
58
+ <MenubarPrimitive.Trigger
59
+ ref={ref}
60
+ className={cn(
61
+ "flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
62
+ className
63
+ )}
64
+ {...props}
65
+ />
66
+ ))
67
+ MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
68
+
69
+ const MenubarSubTrigger = React.forwardRef<
70
+ React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
71
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
72
+ inset?: boolean
73
+ }
74
+ >(({ className, inset, children, ...props }, ref) => (
75
+ <MenubarPrimitive.SubTrigger
76
+ ref={ref}
77
+ className={cn(
78
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
79
+ inset && "pl-8",
80
+ className
81
+ )}
82
+ {...props}
83
+ >
84
+ {children}
85
+ <ChevronRight className="ml-auto h-4 w-4" />
86
+ </MenubarPrimitive.SubTrigger>
87
+ ))
88
+ MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
89
+
90
+ const MenubarSubContent = React.forwardRef<
91
+ React.ElementRef<typeof MenubarPrimitive.SubContent>,
92
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
93
+ >(({ className, ...props }, ref) => (
94
+ <MenubarPrimitive.SubContent
95
+ ref={ref}
96
+ className={cn(
97
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
98
+ className
99
+ )}
100
+ {...props}
101
+ />
102
+ ))
103
+ MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
104
+
105
+ const MenubarContent = React.forwardRef<
106
+ React.ElementRef<typeof MenubarPrimitive.Content>,
107
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
108
+ >(
109
+ (
110
+ { className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
111
+ ref
112
+ ) => (
113
+ <MenubarPrimitive.Portal>
114
+ <MenubarPrimitive.Content
115
+ ref={ref}
116
+ align={align}
117
+ alignOffset={alignOffset}
118
+ sideOffset={sideOffset}
119
+ className={cn(
120
+ "z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
121
+ className
122
+ )}
123
+ {...props}
124
+ />
125
+ </MenubarPrimitive.Portal>
126
+ )
127
+ )
128
+ MenubarContent.displayName = MenubarPrimitive.Content.displayName
129
+
130
+ const MenubarItem = React.forwardRef<
131
+ React.ElementRef<typeof MenubarPrimitive.Item>,
132
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
133
+ inset?: boolean
134
+ }
135
+ >(({ className, inset, ...props }, ref) => (
136
+ <MenubarPrimitive.Item
137
+ ref={ref}
138
+ className={cn(
139
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
140
+ inset && "pl-8",
141
+ className
142
+ )}
143
+ {...props}
144
+ />
145
+ ))
146
+ MenubarItem.displayName = MenubarPrimitive.Item.displayName
147
+
148
+ const MenubarCheckboxItem = React.forwardRef<
149
+ React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
150
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
151
+ >(({ className, children, checked, ...props }, ref) => (
152
+ <MenubarPrimitive.CheckboxItem
153
+ ref={ref}
154
+ className={cn(
155
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
156
+ className
157
+ )}
158
+ checked={checked}
159
+ {...props}
160
+ >
161
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
162
+ <MenubarPrimitive.ItemIndicator>
163
+ <Check className="h-4 w-4" />
164
+ </MenubarPrimitive.ItemIndicator>
165
+ </span>
166
+ {children}
167
+ </MenubarPrimitive.CheckboxItem>
168
+ ))
169
+ MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
170
+
171
+ const MenubarRadioItem = React.forwardRef<
172
+ React.ElementRef<typeof MenubarPrimitive.RadioItem>,
173
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
174
+ >(({ className, children, ...props }, ref) => (
175
+ <MenubarPrimitive.RadioItem
176
+ ref={ref}
177
+ className={cn(
178
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
179
+ className
180
+ )}
181
+ {...props}
182
+ >
183
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
184
+ <MenubarPrimitive.ItemIndicator>
185
+ <Circle className="h-2 w-2 fill-current" />
186
+ </MenubarPrimitive.ItemIndicator>
187
+ </span>
188
+ {children}
189
+ </MenubarPrimitive.RadioItem>
190
+ ))
191
+ MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
192
+
193
+ const MenubarLabel = React.forwardRef<
194
+ React.ElementRef<typeof MenubarPrimitive.Label>,
195
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
196
+ inset?: boolean
197
+ }
198
+ >(({ className, inset, ...props }, ref) => (
199
+ <MenubarPrimitive.Label
200
+ ref={ref}
201
+ className={cn(
202
+ "px-2 py-1.5 text-sm font-semibold",
203
+ inset && "pl-8",
204
+ className
205
+ )}
206
+ {...props}
207
+ />
208
+ ))
209
+ MenubarLabel.displayName = MenubarPrimitive.Label.displayName
210
+
211
+ const MenubarSeparator = React.forwardRef<
212
+ React.ElementRef<typeof MenubarPrimitive.Separator>,
213
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
214
+ >(({ className, ...props }, ref) => (
215
+ <MenubarPrimitive.Separator
216
+ ref={ref}
217
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
218
+ {...props}
219
+ />
220
+ ))
221
+ MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
222
+
223
+ const MenubarShortcut = ({
224
+ className,
225
+ ...props
226
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
227
+ return (
228
+ <span
229
+ className={cn(
230
+ "ml-auto text-xs tracking-widest text-muted-foreground",
231
+ className
232
+ )}
233
+ {...props}
234
+ />
235
+ )
236
+ }
237
+ MenubarShortcut.displayname = "MenubarShortcut"
238
+
239
+ export {
240
+ Menubar,
241
+ MenubarMenu,
242
+ MenubarTrigger,
243
+ MenubarContent,
244
+ MenubarItem,
245
+ MenubarSeparator,
246
+ MenubarLabel,
247
+ MenubarCheckboxItem,
248
+ MenubarRadioGroup,
249
+ MenubarRadioItem,
250
+ MenubarPortal,
251
+ MenubarSubContent,
252
+ MenubarSubTrigger,
253
+ MenubarGroup,
254
+ MenubarSub,
255
+ MenubarShortcut,
256
+ }
client/src/components/ui/navigation-menu.tsx ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3
+ import { cva } from "class-variance-authority"
4
+ import { ChevronDown } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const NavigationMenu = React.forwardRef<
9
+ React.ElementRef<typeof NavigationMenuPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
11
+ >(({ className, children, ...props }, ref) => (
12
+ <NavigationMenuPrimitive.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "relative z-10 flex max-w-max flex-1 items-center justify-center",
16
+ className
17
+ )}
18
+ {...props}
19
+ >
20
+ {children}
21
+ <NavigationMenuViewport />
22
+ </NavigationMenuPrimitive.Root>
23
+ ))
24
+ NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25
+
26
+ const NavigationMenuList = React.forwardRef<
27
+ React.ElementRef<typeof NavigationMenuPrimitive.List>,
28
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
29
+ >(({ className, ...props }, ref) => (
30
+ <NavigationMenuPrimitive.List
31
+ ref={ref}
32
+ className={cn(
33
+ "group flex flex-1 list-none items-center justify-center space-x-1",
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ ))
39
+ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40
+
41
+ const NavigationMenuItem = NavigationMenuPrimitive.Item
42
+
43
+ const navigationMenuTriggerStyle = cva(
44
+ "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
45
+ )
46
+
47
+ const NavigationMenuTrigger = React.forwardRef<
48
+ React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
49
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
50
+ >(({ className, children, ...props }, ref) => (
51
+ <NavigationMenuPrimitive.Trigger
52
+ ref={ref}
53
+ className={cn(navigationMenuTriggerStyle(), "group", className)}
54
+ {...props}
55
+ >
56
+ {children}{" "}
57
+ <ChevronDown
58
+ className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
59
+ aria-hidden="true"
60
+ />
61
+ </NavigationMenuPrimitive.Trigger>
62
+ ))
63
+ NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64
+
65
+ const NavigationMenuContent = React.forwardRef<
66
+ React.ElementRef<typeof NavigationMenuPrimitive.Content>,
67
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
68
+ >(({ className, ...props }, ref) => (
69
+ <NavigationMenuPrimitive.Content
70
+ ref={ref}
71
+ className={cn(
72
+ "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
73
+ className
74
+ )}
75
+ {...props}
76
+ />
77
+ ))
78
+ NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79
+
80
+ const NavigationMenuLink = NavigationMenuPrimitive.Link
81
+
82
+ const NavigationMenuViewport = React.forwardRef<
83
+ React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
84
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
85
+ >(({ className, ...props }, ref) => (
86
+ <div className={cn("absolute left-0 top-full flex justify-center")}>
87
+ <NavigationMenuPrimitive.Viewport
88
+ className={cn(
89
+ "origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
90
+ className
91
+ )}
92
+ ref={ref}
93
+ {...props}
94
+ />
95
+ </div>
96
+ ))
97
+ NavigationMenuViewport.displayName =
98
+ NavigationMenuPrimitive.Viewport.displayName
99
+
100
+ const NavigationMenuIndicator = React.forwardRef<
101
+ React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
102
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
103
+ >(({ className, ...props }, ref) => (
104
+ <NavigationMenuPrimitive.Indicator
105
+ ref={ref}
106
+ className={cn(
107
+ "top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
108
+ className
109
+ )}
110
+ {...props}
111
+ >
112
+ <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
113
+ </NavigationMenuPrimitive.Indicator>
114
+ ))
115
+ NavigationMenuIndicator.displayName =
116
+ NavigationMenuPrimitive.Indicator.displayName
117
+
118
+ export {
119
+ navigationMenuTriggerStyle,
120
+ NavigationMenu,
121
+ NavigationMenuList,
122
+ NavigationMenuItem,
123
+ NavigationMenuContent,
124
+ NavigationMenuTrigger,
125
+ NavigationMenuLink,
126
+ NavigationMenuIndicator,
127
+ NavigationMenuViewport,
128
+ }
client/src/components/ui/pagination.tsx ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { ButtonProps, buttonVariants } from "@/components/ui/button"
6
+
7
+ const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
8
+ <nav
9
+ role="navigation"
10
+ aria-label="pagination"
11
+ className={cn("mx-auto flex w-full justify-center", className)}
12
+ {...props}
13
+ />
14
+ )
15
+ Pagination.displayName = "Pagination"
16
+
17
+ const PaginationContent = React.forwardRef<
18
+ HTMLUListElement,
19
+ React.ComponentProps<"ul">
20
+ >(({ className, ...props }, ref) => (
21
+ <ul
22
+ ref={ref}
23
+ className={cn("flex flex-row items-center gap-1", className)}
24
+ {...props}
25
+ />
26
+ ))
27
+ PaginationContent.displayName = "PaginationContent"
28
+
29
+ const PaginationItem = React.forwardRef<
30
+ HTMLLIElement,
31
+ React.ComponentProps<"li">
32
+ >(({ className, ...props }, ref) => (
33
+ <li ref={ref} className={cn("", className)} {...props} />
34
+ ))
35
+ PaginationItem.displayName = "PaginationItem"
36
+
37
+ type PaginationLinkProps = {
38
+ isActive?: boolean
39
+ } & Pick<ButtonProps, "size"> &
40
+ React.ComponentProps<"a">
41
+
42
+ const PaginationLink = ({
43
+ className,
44
+ isActive,
45
+ size = "icon",
46
+ ...props
47
+ }: PaginationLinkProps) => (
48
+ <a
49
+ aria-current={isActive ? "page" : undefined}
50
+ className={cn(
51
+ buttonVariants({
52
+ variant: isActive ? "outline" : "ghost",
53
+ size,
54
+ }),
55
+ className
56
+ )}
57
+ {...props}
58
+ />
59
+ )
60
+ PaginationLink.displayName = "PaginationLink"
61
+
62
+ const PaginationPrevious = ({
63
+ className,
64
+ ...props
65
+ }: React.ComponentProps<typeof PaginationLink>) => (
66
+ <PaginationLink
67
+ aria-label="Go to previous page"
68
+ size="default"
69
+ className={cn("gap-1 pl-2.5", className)}
70
+ {...props}
71
+ >
72
+ <ChevronLeft className="h-4 w-4" />
73
+ <span>Previous</span>
74
+ </PaginationLink>
75
+ )
76
+ PaginationPrevious.displayName = "PaginationPrevious"
77
+
78
+ const PaginationNext = ({
79
+ className,
80
+ ...props
81
+ }: React.ComponentProps<typeof PaginationLink>) => (
82
+ <PaginationLink
83
+ aria-label="Go to next page"
84
+ size="default"
85
+ className={cn("gap-1 pr-2.5", className)}
86
+ {...props}
87
+ >
88
+ <span>Next</span>
89
+ <ChevronRight className="h-4 w-4" />
90
+ </PaginationLink>
91
+ )
92
+ PaginationNext.displayName = "PaginationNext"
93
+
94
+ const PaginationEllipsis = ({
95
+ className,
96
+ ...props
97
+ }: React.ComponentProps<"span">) => (
98
+ <span
99
+ aria-hidden
100
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
101
+ {...props}
102
+ >
103
+ <MoreHorizontal className="h-4 w-4" />
104
+ <span className="sr-only">More pages</span>
105
+ </span>
106
+ )
107
+ PaginationEllipsis.displayName = "PaginationEllipsis"
108
+
109
+ export {
110
+ Pagination,
111
+ PaginationContent,
112
+ PaginationEllipsis,
113
+ PaginationItem,
114
+ PaginationLink,
115
+ PaginationNext,
116
+ PaginationPrevious,
117
+ }
client/src/components/ui/popover.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Popover = PopoverPrimitive.Root
7
+
8
+ const PopoverTrigger = PopoverPrimitive.Trigger
9
+
10
+ const PopoverContent = React.forwardRef<
11
+ React.ElementRef<typeof PopoverPrimitive.Content>,
12
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
13
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14
+ <PopoverPrimitive.Portal>
15
+ <PopoverPrimitive.Content
16
+ ref={ref}
17
+ align={align}
18
+ sideOffset={sideOffset}
19
+ className={cn(
20
+ "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ </PopoverPrimitive.Portal>
26
+ ))
27
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName
28
+
29
+ export { Popover, PopoverTrigger, PopoverContent }
client/src/components/ui/progress.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as ProgressPrimitive from "@radix-ui/react-progress"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Progress = React.forwardRef<
9
+ React.ElementRef<typeof ProgressPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
11
+ >(({ className, value, ...props }, ref) => (
12
+ <ProgressPrimitive.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "relative h-4 w-full overflow-hidden rounded-full bg-secondary",
16
+ className
17
+ )}
18
+ {...props}
19
+ >
20
+ <ProgressPrimitive.Indicator
21
+ className="h-full w-full flex-1 bg-primary transition-all"
22
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
23
+ />
24
+ </ProgressPrimitive.Root>
25
+ ))
26
+ Progress.displayName = ProgressPrimitive.Root.displayName
27
+
28
+ export { Progress }
client/src/components/ui/radio-group.tsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
3
+ import { Circle } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const RadioGroup = React.forwardRef<
8
+ React.ElementRef<typeof RadioGroupPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
10
+ >(({ className, ...props }, ref) => {
11
+ return (
12
+ <RadioGroupPrimitive.Root
13
+ className={cn("grid gap-2", className)}
14
+ {...props}
15
+ ref={ref}
16
+ />
17
+ )
18
+ })
19
+ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
20
+
21
+ const RadioGroupItem = React.forwardRef<
22
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
23
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
24
+ >(({ className, ...props }, ref) => {
25
+ return (
26
+ <RadioGroupPrimitive.Item
27
+ ref={ref}
28
+ className={cn(
29
+ "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
35
+ <Circle className="h-2.5 w-2.5 fill-current text-current" />
36
+ </RadioGroupPrimitive.Indicator>
37
+ </RadioGroupPrimitive.Item>
38
+ )
39
+ })
40
+ RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
41
+
42
+ export { RadioGroup, RadioGroupItem }