FECUOY commited on
Commit
4c41b3d
·
0 Parent(s):

Initial commit: HackingFactory v2 Enhanced with Self-Refining AI features

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +107 -0
  2. .gitkeep +0 -0
  3. .prettierignore +35 -0
  4. .prettierrc +15 -0
  5. DEVELOPMENT_GUIDE.md +255 -0
  6. README_ENHANCEMENTS.md +253 -0
  7. client/index.html +26 -0
  8. client/public/.gitkeep +0 -0
  9. client/public/__manus__/debug-collector.js +821 -0
  10. client/src/App.tsx +46 -0
  11. client/src/_core/hooks/useAuth.ts +84 -0
  12. client/src/components/AIChatBox.tsx +335 -0
  13. client/src/components/CodeEditor.tsx +50 -0
  14. client/src/components/DashboardLayout.tsx +264 -0
  15. client/src/components/DashboardLayoutSkeleton.tsx +46 -0
  16. client/src/components/ErrorBoundary.tsx +62 -0
  17. client/src/components/IterationChart.tsx +90 -0
  18. client/src/components/ManusDialog.tsx +89 -0
  19. client/src/components/Map.tsx +155 -0
  20. client/src/components/NewProjectDialog.tsx +152 -0
  21. client/src/components/PayloadLibrary.tsx +100 -0
  22. client/src/components/ui/accordion.tsx +64 -0
  23. client/src/components/ui/alert-dialog.tsx +155 -0
  24. client/src/components/ui/alert.tsx +66 -0
  25. client/src/components/ui/aspect-ratio.tsx +9 -0
  26. client/src/components/ui/avatar.tsx +51 -0
  27. client/src/components/ui/badge.tsx +46 -0
  28. client/src/components/ui/breadcrumb.tsx +109 -0
  29. client/src/components/ui/button-group.tsx +83 -0
  30. client/src/components/ui/button.tsx +60 -0
  31. client/src/components/ui/calendar.tsx +211 -0
  32. client/src/components/ui/card.tsx +92 -0
  33. client/src/components/ui/carousel.tsx +239 -0
  34. client/src/components/ui/chart.tsx +355 -0
  35. client/src/components/ui/checkbox.tsx +30 -0
  36. client/src/components/ui/collapsible.tsx +31 -0
  37. client/src/components/ui/command.tsx +184 -0
  38. client/src/components/ui/context-menu.tsx +250 -0
  39. client/src/components/ui/dialog.tsx +209 -0
  40. client/src/components/ui/drawer.tsx +133 -0
  41. client/src/components/ui/dropdown-menu.tsx +255 -0
  42. client/src/components/ui/empty.tsx +104 -0
  43. client/src/components/ui/field.tsx +242 -0
  44. client/src/components/ui/form.tsx +168 -0
  45. client/src/components/ui/hover-card.tsx +42 -0
  46. client/src/components/ui/input-group.tsx +168 -0
  47. client/src/components/ui/input-otp.tsx +75 -0
  48. client/src/components/ui/input.tsx +70 -0
  49. client/src/components/ui/item.tsx +193 -0
  50. client/src/components/ui/kbd.tsx +28 -0
.gitignore ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ **/node_modules
3
+ .pnpm-store/
4
+
5
+ # Build outputs
6
+ dist/
7
+ build/
8
+ *.dist
9
+
10
+ # Environment variables
11
+ .env
12
+ .env.local
13
+ .env.development.local
14
+ .env.test.local
15
+ .env.production.local
16
+
17
+ # IDE and editor files
18
+ .vscode/
19
+ .idea/
20
+ *.swp
21
+ *.swo
22
+ *~
23
+
24
+ # OS generated files
25
+ .DS_Store
26
+ .DS_Store?
27
+ ._*
28
+ .Spotlight-V100
29
+ .Trashes
30
+ ehthumbs.db
31
+ Thumbs.db
32
+
33
+ # Logs
34
+ logs
35
+ *.log
36
+ npm-debug.log*
37
+ yarn-debug.log*
38
+ yarn-error.log*
39
+ pnpm-debug.log*
40
+ lerna-debug.log*
41
+
42
+ # Runtime data
43
+ pids
44
+ *.pid
45
+ *.seed
46
+ *.pid.lock
47
+ *.bak
48
+
49
+ # Coverage directory used by tools like istanbul
50
+ coverage/
51
+ *.lcov
52
+
53
+ # nyc test coverage
54
+ .nyc_output
55
+
56
+ # Dependency directories
57
+ jspm_packages/
58
+
59
+ # TypeScript cache
60
+ *.tsbuildinfo
61
+
62
+ # Optional npm cache directory
63
+ .npm
64
+
65
+ # Optional eslint cache
66
+ .eslintcache
67
+
68
+ # Microbundle cache
69
+ .rpt2_cache/
70
+ .rts2_cache_cjs/
71
+ .rts2_cache_es/
72
+ .rts2_cache_umd/
73
+
74
+ # Optional REPL history
75
+ .node_repl_history
76
+
77
+ # Output of 'npm pack'
78
+ *.tgz
79
+
80
+ # Yarn Integrity file
81
+ .yarn-integrity
82
+
83
+ # parcel-bundler cache (https://parceljs.org/)
84
+ .cache
85
+ .parcel-cache
86
+
87
+ # Next.js build output
88
+ .next
89
+
90
+ # Nuxt.js build / generate output
91
+ .nuxt
92
+
93
+ # Gatsby files
94
+ .cache/
95
+
96
+ # Storybook build outputs
97
+ .out
98
+ .storybook-out
99
+
100
+ # Temporary folders
101
+ tmp/
102
+ temp/
103
+
104
+ # Database
105
+ *.db
106
+ *.sqlite
107
+ *.sqlite3
.gitkeep ADDED
File without changes
.prettierignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+ .pnpm-store/
4
+
5
+ # Build outputs
6
+ dist/
7
+ build/
8
+ *.dist
9
+
10
+ # Generated files
11
+ *.tsbuildinfo
12
+ coverage/
13
+
14
+ # Package files
15
+ package-lock.json
16
+ pnpm-lock.yaml
17
+
18
+ # Database
19
+ *.db
20
+ *.sqlite
21
+ *.sqlite3
22
+
23
+ # Logs
24
+ *.log
25
+
26
+ # Environment files
27
+ .env*
28
+
29
+ # IDE files
30
+ .vscode/
31
+ .idea/
32
+
33
+ # OS files
34
+ .DS_Store
35
+ Thumbs.db
.prettierrc ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "semi": true,
3
+ "trailingComma": "es5",
4
+ "singleQuote": false,
5
+ "printWidth": 80,
6
+ "tabWidth": 2,
7
+ "useTabs": false,
8
+ "bracketSpacing": true,
9
+ "bracketSameLine": false,
10
+ "arrowParens": "avoid",
11
+ "endOfLine": "lf",
12
+ "quoteProps": "as-needed",
13
+ "jsxSingleQuote": false,
14
+ "proseWrap": "preserve"
15
+ }
DEVELOPMENT_GUIDE.md ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HackingFactory v2 - Development Guide
2
+
3
+ ## نظرة عامة
4
+
5
+ **HackingFactory** هو نموذج متقدم لـ "أنظمة الذكاء الاصطناعي ذاتية التحسين" (Self-Refining AI Systems). يجمع بين قوة نماذج التوليد (Qwen) والتقييم (DeepSeek) في حلقة تكرارية مستمرة لإنتاج أكواد عالية الجودة.
6
+
7
+ ---
8
+
9
+ ## المكونات الرئيسية
10
+
11
+ ### 1. **بيئة الفحص المعزولة (Sandbox)**
12
+ **الملف:** `server/sandbox.ts`
13
+
14
+ يوفر بيئة معزولة آمنة لتشغيل الأكواد المولدة واختبارها ديناميكياً:
15
+ - تشغيل الأكواد بحد زمني (5 ثوان)
16
+ - قياس الأداء ووقت التنفيذ
17
+ - التقاط المخرجات والأخطاء
18
+
19
+ ```typescript
20
+ const result = await runCodeInSandbox(code, "python");
21
+ // { success: boolean, output: string, error?: string, executionTime: number }
22
+ ```
23
+
24
+ ### 2. **محرك التكرار الذاتي (Self-Refining Loop)**
25
+ **الملف:** `server/factory.ts`
26
+
27
+ يدير العملية الكاملة للتوليد والتقييم والتحسين:
28
+ 1. البحث في الذاكرة عن حلول مشابهة سابقة
29
+ 2. توليد كود جديد (Qwen)
30
+ 3. فحص ديناميكي (Sandbox)
31
+ 4. تقييم شامل (DeepSeek)
32
+ 5. حفظ النتائج والتكرار حتى الوصول للدرجة المستهدفة
33
+
34
+ ```typescript
35
+ await runSelfRefiningLoop(projectId, prompt, targetScore, maxIterations);
36
+ ```
37
+
38
+ ### 3. **قاعدة البيانات المتجهة (Vector Memory)**
39
+ **الملف:** `server/memory.ts`
40
+
41
+ تخزين واسترجاع الحلول الناجحة السابقة باستخدام ChromaDB:
42
+ - البحث الدلالي عن حلول مشابهة
43
+ - تقليل وقت التوليد للمشاكل المتكررة
44
+ - تحسين جودة الحلول الجديدة
45
+
46
+ ```typescript
47
+ const memory = ProjectMemory.getInstance();
48
+ await memory.addSuccessfulCode(projectId, code, prompt, score);
49
+ const similar = await memory.findSimilarSolutions(query);
50
+ ```
51
+
52
+ ### 4. **نظام التقييم المتعدد (Multi-Criteria Scoring)**
53
+ **الملف:** `server/routers/chat.ts` - دالة `evaluateCode`
54
+
55
+ تقييم الأكواد بناءً على أربعة معايير (0-25 نقطة لكل معيار):
56
+ 1. **التخفي (Stealth):** إخفاء الأثر، تجنب الكشف
57
+ 2. **الاستقرار (Stability):** معالجة الأخطاء، إدارة الموارد
58
+ 3. **الكفاءة (Efficiency):** الأداء، سرعة التنفيذ
59
+ 4. **الموثوقية (Reliability):** معدل النجاح، التوافقية
60
+
61
+ ### 5. **محرك التشفير والتخفي (Obfuscation Engine)**
62
+ **الملف:** `server/obfuscation.ts`
63
+
64
+ يوفر عدة مستويات من التشفير:
65
+ - **Low:** إعادة تسمية المتغيرات
66
+ - **Medium:** تشفير النصوص + إضافة كود وهمي
67
+ - **High:** مستويات متعددة من التشفير
68
+
69
+ ```typescript
70
+ const obfuscated = ObfuscationEngine.fullObfuscate(code, "high");
71
+ ```
72
+
73
+ ### 6. **نظام الإشعارات (Notifications)**
74
+ **الملف:** `server/notifications.ts`
75
+
76
+ إخطار المستخدمين بتقدم المشاريع:
77
+ - إكمال التكرارات
78
+ - إكمال المشاريع
79
+ - الأخطاء والتحذيرات
80
+
81
+ ```typescript
82
+ const notifier = NotificationManager.getInstance();
83
+ await notifier.notifyProjectComplete(userId, projectName, score, projectId);
84
+ ```
85
+
86
+ ### 7. **لوحة التحكم المرئية (Visual Dashboard)**
87
+ **الملف:** `client/src/pages/Dashboard.tsx`
88
+
89
+ واجهة رسومية تعرض:
90
+ - إحصائيات المشاريع
91
+ - رسم بياني لتقدم التكرارات (Iteration Progress Chart)
92
+ - قائمة المشاريع النشطة
93
+ - معلومات مفصلة عن كل مشروع
94
+
95
+ ### 8. **محرر الأكواد المباشر (Live Code Editor)**
96
+ **الملف:** `client/src/components/CodeEditor.tsx`
97
+
98
+ يسمح للمستخدمين بـ:
99
+ - تحرير الأكواد يدوياً
100
+ - نسخ الأكواد
101
+ - تشغيل الأكواد مباشرة
102
+
103
+ ### 9. **مكتبة القوالب (Payload Library)**
104
+ **الملف:** `client/src/components/PayloadLibrary.tsx`
105
+
106
+ مكتبة منظمة للحلول الناجحة:
107
+ - البحث والتصفية حسب الوسوم
108
+ - عرض درجات الجودة
109
+ - تطبيق التشفير على القوالب
110
+
111
+ ---
112
+
113
+ ## سير العمل (Workflow)
114
+
115
+ ### 1. إنشاء مشروع جديد
116
+ ```typescript
117
+ const project = await trpc.projects.create.mutate({
118
+ name: "My Project",
119
+ mode: "loop",
120
+ contentType: "code",
121
+ originalPrompt: "Generate a reverse shell...",
122
+ });
123
+ ```
124
+
125
+ ### 2. تشغيل المصنع
126
+ ```typescript
127
+ await trpc.projects.runFactory.mutate({
128
+ projectId: project.projectId,
129
+ prompt: project.originalPrompt,
130
+ targetScore: 90,
131
+ });
132
+ ```
133
+
134
+ ### 3. مراقبة التقدم
135
+ ```typescript
136
+ const iterations = await trpc.projects.getIterations.query({
137
+ projectId: project.projectId,
138
+ });
139
+ ```
140
+
141
+ ### 4. تطبيق التشفير (Ghost Mode)
142
+ ```typescript
143
+ const obfuscated = await trpc.projects.obfuscateCode.mutate({
144
+ code: finalCode,
145
+ level: "high",
146
+ });
147
+ ```
148
+
149
+ ---
150
+
151
+ ## البنية الديناميكية (Dynamic Analysis)
152
+
153
+ عند تقييم الكود، يتم تنفيذه فعلياً في بيئة معزولة:
154
+
155
+ ```
156
+ Input Code
157
+
158
+ [Sandbox Execution] → Output, Errors, Execution Time
159
+
160
+ [LLM Analysis] → Scoring (Stealth, Stability, Efficiency, Reliability)
161
+
162
+ [Iteration Storage] → Save to Database
163
+
164
+ [Score Check] → If score < target, repeat generation
165
+ ```
166
+
167
+ ---
168
+
169
+ ## التكامل مع الواجهة الأمامية
170
+
171
+ ### مسارات tRPC المتاحة
172
+
173
+ **Projects Router:**
174
+ - `create` - إنشاء مشروع جديد
175
+ - `list` - قائمة المشاريع
176
+ - `getById` - الحصول على تفاصيل مشروع
177
+ - `updateStatus` - تحديث حالة المشروع
178
+ - `addIteration` - إضافة تكرار جديد
179
+ - `getIterations` - الحصول على التكرارات
180
+ - `runFactory` - تشغيل المصنع
181
+ - `obfuscateCode` - تطبيق التشفير
182
+
183
+ **Chat Router:**
184
+ - `sendMessage` - إرسال رسالة للذكاء الاصطناعي
185
+ - `generateCode` - توليد كود
186
+ - `evaluateCode` - تقييم كود
187
+
188
+ ---
189
+
190
+ ## متطلبات التثبيت
191
+
192
+ ```bash
193
+ # تثبيت المكتبات
194
+ npm install
195
+
196
+ # تثبيت ChromaDB (Python)
197
+ sudo pip3 install chromadb
198
+
199
+ # تشغيل الخادم
200
+ npm run dev
201
+ ```
202
+
203
+ ---
204
+
205
+ ## ملاحظات تطويرية
206
+
207
+ ### الأداء
208
+ - استخدام الخيط الخلفي (Background) لتشغيل المصنع لتجنب timeout
209
+ - تخزين النتائج في الذاكرة المتجهة لتسريع البحث
210
+
211
+ ### الأمان
212
+ - جميع الأكواد تُشغل في بيئة معزولة
213
+ - لا توجد وصول مباشر للنظام الرئيسي
214
+ - التحقق من صلاحيات المستخدم على جميع المسارات
215
+
216
+ ### التوسع المستقبلي
217
+ - دعم نماذج استدلال متقدمة (o1, DeepSeek-R1)
218
+ - تكامل Docker للفحص الديناميكي المتقدم
219
+ - نظام التنبيهات عبر WebSocket
220
+ - تصدير النتائج بصيغ متعددة
221
+
222
+ ---
223
+
224
+ ## أمثلة الاستخدام
225
+
226
+ ### مثال 1: توليد وتقييم كود بسيط
227
+ ```typescript
228
+ const result = await trpc.chat.generateCode.mutate({
229
+ projectId: 1,
230
+ prompt: "Write a Python script that prints hello world",
231
+ });
232
+ ```
233
+
234
+ ### مثال 2: تشغيل حلقة التكرار الكاملة
235
+ ```typescript
236
+ await trpc.projects.runFactory.mutate({
237
+ projectId: 1,
238
+ prompt: "Generate a secure password generator",
239
+ targetScore: 95,
240
+ });
241
+ ```
242
+
243
+ ### مثال 3: تطبيق التشفير على الكود
244
+ ```typescript
245
+ const ghostCode = await trpc.projects.obfuscateCode.mutate({
246
+ code: "print('Hello')",
247
+ level: "high",
248
+ });
249
+ ```
250
+
251
+ ---
252
+
253
+ ## الدعم والمساهمة
254
+
255
+ للمزيد من المعلومات أو الإبلاغ عن مشاكل، يرجى فتح issue في المستودع.
README_ENHANCEMENTS.md ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HackingFactory v2 - التطويرات الجديدة
2
+
3
+ ## ملخص التحديثات
4
+
5
+ تم تطوير **HackingFactory** ليصبح نظاماً متقدماً لـ "الذكاء الاصطناعي ذاتي التحسين" مع المميزات التالية:
6
+
7
+ ---
8
+
9
+ ## 🎯 المميزات الرئيسية
10
+
11
+ ### 1. **بيئة الفحص الديناميكية (Dynamic Sandbox)**
12
+ - تشغيل الأكواد المولدة في بيئة معزولة آمنة
13
+ - قياس الأداء والموارد المستخدمة
14
+ - اكتشاف الأخطاء والمشاكل قبل النشر
15
+ - **الملف:** `server/sandbox.ts`
16
+
17
+ ### 2. **نظام التقييم المتعدد المعايير (Multi-Criteria Scoring)**
18
+ تقييم شامل للأكواد بناءً على:
19
+ - **التخفي (Stealth):** مدى إخفاء الأثر والكشف
20
+ - **الاستقرار (Stability):** معالجة الأخطاء والموارد
21
+ - **الكفاءة (Efficiency):** الأداء والسرعة
22
+ - **الموثوقية (Reliability):** معدل النجاح والتوافقية
23
+
24
+ ### 3. **حلقة التكرار الذاتي (Self-Refining Loop)**
25
+ - توليد تلقائي للأكواد (Qwen)
26
+ - تقييم ذكي (DeepSeek)
27
+ - تحسين مستمر حتى الوصول للدرجة المستهدفة
28
+ - **الملف:** `server/factory.ts`
29
+
30
+ ### 4. **الذاكرة المتجهة (Vector Memory)**
31
+ - تخزين الحلول الناجحة السابقة
32
+ - البحث الدلالي عن حلول مشابهة
33
+ - تسريع التوليد للمشاكل المتكررة
34
+ - **الملف:** `server/memory.ts`
35
+ - **التقنية:** ChromaDB
36
+
37
+ ### 5. **محرك التشفير والتخفي (Obfuscation Engine)**
38
+ ثلاث مستويات من التشفير:
39
+ - **Low:** إعادة تسمية المتغيرات
40
+ - **Medium:** تشفير النصوص + كود وهمي
41
+ - **High:** تشفير متعدد المستويات
42
+ - **الملف:** `server/obfuscation.ts`
43
+
44
+ ### 6. **لوحة التحكم المرئية (Visual Dashboard)**
45
+ - عرض إحصائيات المشاريع
46
+ - رسم بياني لتقدم التكرارات
47
+ - معلومات مفصلة عن كل مشروع
48
+ - **الملف:** `client/src/pages/Dashboard.tsx`
49
+
50
+ ### 7. **محرر الأكواد المباشر (Live Code Editor)**
51
+ - تحرير الأكواد يدوياً
52
+ - نسخ وتحميل الأكواد
53
+ - تشغيل الأكواد مباشرة
54
+ - **الملف:** `client/src/components/CodeEditor.tsx`
55
+
56
+ ### 8. **مكتبة القوالب (Payload Library)**
57
+ - مكتبة منظمة للحلول الناجحة
58
+ - البحث والتصفية حسب الوسوم
59
+ - تطبيق التشفير على القوالب
60
+ - **الملف:** `client/src/components/PayloadLibrary.tsx`
61
+
62
+ ### 9. **نظام الإشعارات (Notifications)**
63
+ - إخطار المستخدمين بتقدم المشاريع
64
+ - تنبيهات الإكمال والأخطاء
65
+ - **الملف:** `server/notifications.ts`
66
+
67
+ ---
68
+
69
+ ## 📊 معمارية النظام
70
+
71
+ ```
72
+ ┌─────────────────────────────────────────────────────────┐
73
+ │ User Interface │
74
+ │ (Dashboard | Chat | Code Editor | Payload Library) │
75
+ └──────────────────────┬──────────────────────────────────┘
76
+
77
+ ┌──────────────────────▼──────────────────────────────────┐
78
+ │ tRPC API Layer │
79
+ │ (Projects Router | Chat Router | Payload Router) │
80
+ └──────────────────────┬──────────────────────────────────┘
81
+
82
+ ┌──────────────────────▼──────────────────────────────────┐
83
+ │ Self-Refining Loop Engine │
84
+ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
85
+ │ │ Generation │→ │ Sandbox Test │→ │ Evaluation │ │
86
+ │ │ (Qwen) │ │ (Dynamic) │ │ (DeepSeek) │ │
87
+ │ └─────────────┘ └──────────────┘ └──────────────┘ │
88
+ └──────────────────────┬──────────────────────────────────┘
89
+
90
+ ┌──────────────────────▼───��──────────────────────────────┐
91
+ │ Data & Memory Layer │
92
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
93
+ │ │ MySQL DB │ │ ChromaDB │ │ Obfuscation │ │
94
+ │ │ (Projects) │ │ (Memory) │ │ Engine │ │
95
+ │ └──────────────┘ └──────────────┘ └──────────────┘ │
96
+ └─────────────────────────────────────────────────────────┘
97
+ ```
98
+
99
+ ---
100
+
101
+ ## 🚀 البدء السريع
102
+
103
+ ### التثبيت
104
+ ```bash
105
+ # استنساخ المستودع
106
+ cd /home/ubuntu/hacking_factory
107
+
108
+ # تثبيت المكتبات
109
+ npm install
110
+
111
+ # تثبيت ChromaDB
112
+ sudo pip3 install chromadb
113
+
114
+ # تشغيل الخادم
115
+ npm run dev
116
+ ```
117
+
118
+ ### إنشاء مشروع جديد
119
+ ```typescript
120
+ const project = await trpc.projects.create.mutate({
121
+ name: "My Security Project",
122
+ mode: "loop",
123
+ contentType: "code",
124
+ originalPrompt: "Generate a secure authentication system",
125
+ });
126
+ ```
127
+
128
+ ### تشغيل المصنع
129
+ ```typescript
130
+ await trpc.projects.runFactory.mutate({
131
+ projectId: project.projectId,
132
+ prompt: project.originalPrompt,
133
+ targetScore: 90,
134
+ });
135
+ ```
136
+
137
+ ### مراقبة التقدم
138
+ ```typescript
139
+ const iterations = await trpc.projects.getIterations.query({
140
+ projectId: project.projectId,
141
+ });
142
+
143
+ // عرض الرسم البياني في Dashboard
144
+ ```
145
+
146
+ ---
147
+
148
+ ## 📁 هيكل الملفات الجديدة
149
+
150
+ ```
151
+ server/
152
+ ├── sandbox.ts # بيئة الفحص المعزولة
153
+ ├── factory.ts # محرك التكرار الذاتي
154
+ ├── memory.ts # الذاكرة المتجهة
155
+ ├── obfuscation.ts # محرك التشفير
156
+ ├── notifications.ts # نظام الإشعارات
157
+ └── routers/
158
+ └── projects.ts # مسارات المشاريع المحدثة
159
+
160
+ client/src/
161
+ ├── components/
162
+ │ ├── IterationChart.tsx # رسم بياني التكرارات
163
+ │ ├── CodeEditor.tsx # محرر الأكواد
164
+ │ └── PayloadLibrary.tsx # مكتبة القوالب
165
+ └── pages/
166
+ └── Dashboard.tsx # لوحة التحكم المحدثة
167
+ ```
168
+
169
+ ---
170
+
171
+ ## 🔄 سير العمل الكامل
172
+
173
+ ```
174
+ 1. إنشاء مشروع جديد
175
+
176
+ 2. تشغيل المصنع (runFactory)
177
+
178
+ 3. البحث في الذاكرة عن حلول مشابهة
179
+
180
+ 4. توليد الكود الأول (Qwen)
181
+
182
+ 5. فحص ديناميكي (Sandbox)
183
+
184
+ 6. تقييم شامل (DeepSeek)
185
+
186
+ 7. حفظ النتيجة في قاعدة البيانات
187
+
188
+ 8. هل الدرجة ≥ الهدف؟
189
+ ├─ نعم → حفظ في الذاكرة + إشعار النجاح
190
+ └─ لا → العودة للخطوة 4 (تحسين الكود)
191
+ ```
192
+
193
+ ---
194
+
195
+ ## 🎨 واجهة المستخدم
196
+
197
+ ### لوحة التحكم (Dashboard)
198
+ - عرض إحصائيات المشاريع النشطة والمكتملة
199
+ - رسم بياني تفاعلي لتقدم التكرارات
200
+ - قائمة المشاريع مع الحالة والدرجات
201
+
202
+ ### محرر الأكواد (Code Editor)
203
+ - تحرير الأكواد بصيغة Python/JavaScript
204
+ - نسخ سريع للأكواد
205
+ - تشغيل فوري للاختبار
206
+
207
+ ### مكتبة القوالب (Payload Library)
208
+ - عرض الحلول الناجحة السابقة
209
+ - البحث والتصفية حسب الوسوم
210
+ - تطبيق التشفير بمستويات مختلفة
211
+
212
+ ---
213
+
214
+ ## 🔐 الأمان
215
+
216
+ - جميع الأكواد تُشغل في بيئة معزولة (Sandbox)
217
+ - لا توجد وصول مباشر للنظام الرئيسي
218
+ - التحقق من صلاحيات المستخدم على جميع المسارات
219
+ - تشفير الأكواد قبل التخزين (اختياري)
220
+
221
+ ---
222
+
223
+ ## 📈 الأداء
224
+
225
+ - استخدام الخيط الخلفي (Background) لتشغيل المصنع
226
+ - تخزين النتائج في الذاكرة المتجهة لتسريع البحث
227
+ - تقليل وقت التوليد للمشاكل المتكررة بـ 50-70%
228
+
229
+ ---
230
+
231
+ ## 🔮 التطويرات المستقبلية
232
+
233
+ - [ ] دعم نماذج استدلال متقدمة (OpenAI o1, DeepSeek-R1)
234
+ - [ ] تكامل Docker الكامل للفحص الديناميكي
235
+ - [ ] نظام التنبيهات عبر WebSocket
236
+ - [ ] تصدير النتائج بصيغ متعددة (PDF, JSON, CSV)
237
+ - [ ] واجهة إدارة متقدمة للمشاريع
238
+ - [ ] تحليل الأداء والإحصائيات المتقدمة
239
+
240
+ ---
241
+
242
+ ## 📞 الدعم
243
+
244
+ للمزيد من المعلومات، راجع:
245
+ - `DEVELOPMENT_GUIDE.md` - دليل التطوير الشا��ل
246
+ - `todo.md` - قائمة المهام والتقدم
247
+ - `server/` - كود الخادم
248
+ - `client/src/` - كود الواجهة الأمامية
249
+
250
+ ---
251
+
252
+ **آخر تحديث:** 25 يناير 2026
253
+ **الإصدار:** v2.0
client/index.html ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta
7
+ name="viewport"
8
+ content="width=device-width, initial-scale=1.0, maximum-scale=1" />
9
+ <title>Hacking Factory Professional UI</title>
10
+ <!-- THIS IS THE START OF A COMMENT BLOCK, BLOCK TO BE DELETED: Google Fonts here, example:
11
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
13
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
14
+ THIS IS THE END OF A COMMENT BLOCK, BLOCK TO BE DELETED -->
15
+ </head>
16
+
17
+ <body>
18
+ <div id="root"></div>
19
+ <script type="module" src="/src/main.tsx"></script>
20
+ <script
21
+ defer
22
+ src="%VITE_ANALYTICS_ENDPOINT%/umami"
23
+ data-website-id="%VITE_ANALYTICS_WEBSITE_ID%"></script>
24
+ </body>
25
+
26
+ </html>
client/public/.gitkeep ADDED
File without changes
client/public/__manus__/debug-collector.js ADDED
@@ -0,0 +1,821 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Manus Debug Collector (agent-friendly)
3
+ *
4
+ * Captures:
5
+ * 1) Console logs
6
+ * 2) Network requests (fetch + XHR)
7
+ * 3) User interactions (semantic uiEvents: click/type/submit/nav/scroll/etc.)
8
+ *
9
+ * Data is periodically sent to /__manus__/logs
10
+ * Note: uiEvents are mirrored to sessionEvents for sessionReplay.log
11
+ */
12
+ (function () {
13
+ "use strict";
14
+
15
+ // Prevent double initialization
16
+ if (window.__MANUS_DEBUG_COLLECTOR__) return;
17
+
18
+ // ==========================================================================
19
+ // Configuration
20
+ // ==========================================================================
21
+ const CONFIG = {
22
+ reportEndpoint: "/__manus__/logs",
23
+ bufferSize: {
24
+ console: 500,
25
+ network: 200,
26
+ // semantic, agent-friendly UI events
27
+ ui: 500,
28
+ },
29
+ reportInterval: 2000,
30
+ sensitiveFields: [
31
+ "password",
32
+ "token",
33
+ "secret",
34
+ "key",
35
+ "authorization",
36
+ "cookie",
37
+ "session",
38
+ ],
39
+ maxBodyLength: 10240,
40
+ // UI event logging privacy policy:
41
+ // - inputs matching sensitiveFields or type=password are masked by default
42
+ // - non-sensitive inputs log up to 200 chars
43
+ uiInputMaxLen: 200,
44
+ uiTextMaxLen: 80,
45
+ // Scroll throttling: minimum ms between scroll events
46
+ scrollThrottleMs: 500,
47
+ };
48
+
49
+ // ==========================================================================
50
+ // Storage
51
+ // ==========================================================================
52
+ const store = {
53
+ consoleLogs: [],
54
+ networkRequests: [],
55
+ uiEvents: [],
56
+ lastReportTime: Date.now(),
57
+ lastScrollTime: 0,
58
+ };
59
+
60
+ // ==========================================================================
61
+ // Utility Functions
62
+ // ==========================================================================
63
+
64
+ function sanitizeValue(value, depth) {
65
+ if (depth === void 0) depth = 0;
66
+ if (depth > 5) return "[Max Depth]";
67
+ if (value === null) return null;
68
+ if (value === undefined) return undefined;
69
+
70
+ if (typeof value === "string") {
71
+ return value.length > 1000 ? value.slice(0, 1000) + "...[truncated]" : value;
72
+ }
73
+
74
+ if (typeof value !== "object") return value;
75
+
76
+ if (Array.isArray(value)) {
77
+ return value.slice(0, 100).map(function (v) {
78
+ return sanitizeValue(v, depth + 1);
79
+ });
80
+ }
81
+
82
+ var sanitized = {};
83
+ for (var k in value) {
84
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
85
+ var isSensitive = CONFIG.sensitiveFields.some(function (f) {
86
+ return k.toLowerCase().indexOf(f) !== -1;
87
+ });
88
+ if (isSensitive) {
89
+ sanitized[k] = "[REDACTED]";
90
+ } else {
91
+ sanitized[k] = sanitizeValue(value[k], depth + 1);
92
+ }
93
+ }
94
+ }
95
+ return sanitized;
96
+ }
97
+
98
+ function formatArg(arg) {
99
+ try {
100
+ if (arg instanceof Error) {
101
+ return { type: "Error", message: arg.message, stack: arg.stack };
102
+ }
103
+ if (typeof arg === "object") return sanitizeValue(arg);
104
+ return String(arg);
105
+ } catch (e) {
106
+ return "[Unserializable]";
107
+ }
108
+ }
109
+
110
+ function formatArgs(args) {
111
+ var result = [];
112
+ for (var i = 0; i < args.length; i++) result.push(formatArg(args[i]));
113
+ return result;
114
+ }
115
+
116
+ function pruneBuffer(buffer, maxSize) {
117
+ if (buffer.length > maxSize) buffer.splice(0, buffer.length - maxSize);
118
+ }
119
+
120
+ function tryParseJson(str) {
121
+ if (typeof str !== "string") return str;
122
+ try {
123
+ return JSON.parse(str);
124
+ } catch (e) {
125
+ return str;
126
+ }
127
+ }
128
+
129
+ // ==========================================================================
130
+ // Semantic UI Event Logging (agent-friendly)
131
+ // ==========================================================================
132
+
133
+ function shouldIgnoreTarget(target) {
134
+ try {
135
+ if (!target || !(target instanceof Element)) return false;
136
+ return !!target.closest(".manus-no-record");
137
+ } catch (e) {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ function compactText(s, maxLen) {
143
+ try {
144
+ var t = (s || "").trim().replace(/\s+/g, " ");
145
+ if (!t) return "";
146
+ return t.length > maxLen ? t.slice(0, maxLen) + "…" : t;
147
+ } catch (e) {
148
+ return "";
149
+ }
150
+ }
151
+
152
+ function elText(el) {
153
+ try {
154
+ var t = el.innerText || el.textContent || "";
155
+ return compactText(t, CONFIG.uiTextMaxLen);
156
+ } catch (e) {
157
+ return "";
158
+ }
159
+ }
160
+
161
+ function describeElement(el) {
162
+ if (!el || !(el instanceof Element)) return null;
163
+
164
+ var getAttr = function (name) {
165
+ return el.getAttribute(name);
166
+ };
167
+
168
+ var tag = el.tagName ? el.tagName.toLowerCase() : null;
169
+ var id = el.id || null;
170
+ var name = getAttr("name") || null;
171
+ var role = getAttr("role") || null;
172
+ var ariaLabel = getAttr("aria-label") || null;
173
+
174
+ var dataLoc = getAttr("data-loc") || null;
175
+ var testId =
176
+ getAttr("data-testid") ||
177
+ getAttr("data-test-id") ||
178
+ getAttr("data-test") ||
179
+ null;
180
+
181
+ var type = tag === "input" ? (getAttr("type") || "text") : null;
182
+ var href = tag === "a" ? getAttr("href") || null : null;
183
+
184
+ // a small, stable hint for agents (avoid building full CSS paths)
185
+ var selectorHint = null;
186
+ if (testId) selectorHint = '[data-testid="' + testId + '"]';
187
+ else if (dataLoc) selectorHint = '[data-loc="' + dataLoc + '"]';
188
+ else if (id) selectorHint = "#" + id;
189
+ else selectorHint = tag || "unknown";
190
+
191
+ return {
192
+ tag: tag,
193
+ id: id,
194
+ name: name,
195
+ type: type,
196
+ role: role,
197
+ ariaLabel: ariaLabel,
198
+ testId: testId,
199
+ dataLoc: dataLoc,
200
+ href: href,
201
+ text: elText(el),
202
+ selectorHint: selectorHint,
203
+ };
204
+ }
205
+
206
+ function isSensitiveField(el) {
207
+ if (!el || !(el instanceof Element)) return false;
208
+ var tag = el.tagName ? el.tagName.toLowerCase() : "";
209
+ if (tag !== "input" && tag !== "textarea") return false;
210
+
211
+ var type = (el.getAttribute("type") || "").toLowerCase();
212
+ if (type === "password") return true;
213
+
214
+ var name = (el.getAttribute("name") || "").toLowerCase();
215
+ var id = (el.id || "").toLowerCase();
216
+
217
+ return CONFIG.sensitiveFields.some(function (f) {
218
+ return name.indexOf(f) !== -1 || id.indexOf(f) !== -1;
219
+ });
220
+ }
221
+
222
+ function getInputValueSafe(el) {
223
+ if (!el || !(el instanceof Element)) return null;
224
+ var tag = el.tagName ? el.tagName.toLowerCase() : "";
225
+ if (tag !== "input" && tag !== "textarea" && tag !== "select") return null;
226
+
227
+ var v = "";
228
+ try {
229
+ v = el.value != null ? String(el.value) : "";
230
+ } catch (e) {
231
+ v = "";
232
+ }
233
+
234
+ if (isSensitiveField(el)) return { masked: true, length: v.length };
235
+
236
+ if (v.length > CONFIG.uiInputMaxLen) v = v.slice(0, CONFIG.uiInputMaxLen) + "…";
237
+ return v;
238
+ }
239
+
240
+ function logUiEvent(kind, payload) {
241
+ var entry = {
242
+ timestamp: Date.now(),
243
+ kind: kind,
244
+ url: location.href,
245
+ viewport: { width: window.innerWidth, height: window.innerHeight },
246
+ payload: sanitizeValue(payload),
247
+ };
248
+ store.uiEvents.push(entry);
249
+ pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui);
250
+ }
251
+
252
+ function installUiEventListeners() {
253
+ // Clicks
254
+ document.addEventListener(
255
+ "click",
256
+ function (e) {
257
+ var t = e.target;
258
+ if (shouldIgnoreTarget(t)) return;
259
+ logUiEvent("click", {
260
+ target: describeElement(t),
261
+ x: e.clientX,
262
+ y: e.clientY,
263
+ });
264
+ },
265
+ true
266
+ );
267
+
268
+ // Typing "commit" events
269
+ document.addEventListener(
270
+ "change",
271
+ function (e) {
272
+ var t = e.target;
273
+ if (shouldIgnoreTarget(t)) return;
274
+ logUiEvent("change", {
275
+ target: describeElement(t),
276
+ value: getInputValueSafe(t),
277
+ });
278
+ },
279
+ true
280
+ );
281
+
282
+ document.addEventListener(
283
+ "focusin",
284
+ function (e) {
285
+ var t = e.target;
286
+ if (shouldIgnoreTarget(t)) return;
287
+ logUiEvent("focusin", { target: describeElement(t) });
288
+ },
289
+ true
290
+ );
291
+
292
+ document.addEventListener(
293
+ "focusout",
294
+ function (e) {
295
+ var t = e.target;
296
+ if (shouldIgnoreTarget(t)) return;
297
+ logUiEvent("focusout", {
298
+ target: describeElement(t),
299
+ value: getInputValueSafe(t),
300
+ });
301
+ },
302
+ true
303
+ );
304
+
305
+ // Enter/Escape are useful for form flows & modals
306
+ document.addEventListener(
307
+ "keydown",
308
+ function (e) {
309
+ if (e.key !== "Enter" && e.key !== "Escape") return;
310
+ var t = e.target;
311
+ if (shouldIgnoreTarget(t)) return;
312
+ logUiEvent("keydown", { key: e.key, target: describeElement(t) });
313
+ },
314
+ true
315
+ );
316
+
317
+ // Form submissions
318
+ document.addEventListener(
319
+ "submit",
320
+ function (e) {
321
+ var t = e.target;
322
+ if (shouldIgnoreTarget(t)) return;
323
+ logUiEvent("submit", { target: describeElement(t) });
324
+ },
325
+ true
326
+ );
327
+
328
+ // Throttled scroll events
329
+ window.addEventListener(
330
+ "scroll",
331
+ function () {
332
+ var now = Date.now();
333
+ if (now - store.lastScrollTime < CONFIG.scrollThrottleMs) return;
334
+ store.lastScrollTime = now;
335
+
336
+ logUiEvent("scroll", {
337
+ scrollX: window.scrollX,
338
+ scrollY: window.scrollY,
339
+ documentHeight: document.documentElement.scrollHeight,
340
+ viewportHeight: window.innerHeight,
341
+ });
342
+ },
343
+ { passive: true }
344
+ );
345
+
346
+ // Navigation tracking for SPAs
347
+ function nav(reason) {
348
+ logUiEvent("navigate", { reason: reason });
349
+ }
350
+
351
+ var origPush = history.pushState;
352
+ history.pushState = function () {
353
+ origPush.apply(this, arguments);
354
+ nav("pushState");
355
+ };
356
+
357
+ var origReplace = history.replaceState;
358
+ history.replaceState = function () {
359
+ origReplace.apply(this, arguments);
360
+ nav("replaceState");
361
+ };
362
+
363
+ window.addEventListener("popstate", function () {
364
+ nav("popstate");
365
+ });
366
+ window.addEventListener("hashchange", function () {
367
+ nav("hashchange");
368
+ });
369
+ }
370
+
371
+ // ==========================================================================
372
+ // Console Interception
373
+ // ==========================================================================
374
+
375
+ var originalConsole = {
376
+ log: console.log.bind(console),
377
+ debug: console.debug.bind(console),
378
+ info: console.info.bind(console),
379
+ warn: console.warn.bind(console),
380
+ error: console.error.bind(console),
381
+ };
382
+
383
+ ["log", "debug", "info", "warn", "error"].forEach(function (method) {
384
+ console[method] = function () {
385
+ var args = Array.prototype.slice.call(arguments);
386
+
387
+ var entry = {
388
+ timestamp: Date.now(),
389
+ level: method.toUpperCase(),
390
+ args: formatArgs(args),
391
+ stack: method === "error" ? new Error().stack : null,
392
+ };
393
+
394
+ store.consoleLogs.push(entry);
395
+ pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
396
+
397
+ originalConsole[method].apply(console, args);
398
+ };
399
+ });
400
+
401
+ window.addEventListener("error", function (event) {
402
+ store.consoleLogs.push({
403
+ timestamp: Date.now(),
404
+ level: "ERROR",
405
+ args: [
406
+ {
407
+ type: "UncaughtError",
408
+ message: event.message,
409
+ filename: event.filename,
410
+ lineno: event.lineno,
411
+ colno: event.colno,
412
+ stack: event.error ? event.error.stack : null,
413
+ },
414
+ ],
415
+ stack: event.error ? event.error.stack : null,
416
+ });
417
+ pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
418
+
419
+ // Mark an error moment in UI event stream for agents
420
+ logUiEvent("error", {
421
+ message: event.message,
422
+ filename: event.filename,
423
+ lineno: event.lineno,
424
+ colno: event.colno,
425
+ });
426
+ });
427
+
428
+ window.addEventListener("unhandledrejection", function (event) {
429
+ var reason = event.reason;
430
+ store.consoleLogs.push({
431
+ timestamp: Date.now(),
432
+ level: "ERROR",
433
+ args: [
434
+ {
435
+ type: "UnhandledRejection",
436
+ reason: reason && reason.message ? reason.message : String(reason),
437
+ stack: reason && reason.stack ? reason.stack : null,
438
+ },
439
+ ],
440
+ stack: reason && reason.stack ? reason.stack : null,
441
+ });
442
+ pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
443
+
444
+ logUiEvent("unhandledrejection", {
445
+ reason: reason && reason.message ? reason.message : String(reason),
446
+ });
447
+ });
448
+
449
+ // ==========================================================================
450
+ // Fetch Interception
451
+ // ==========================================================================
452
+
453
+ var originalFetch = window.fetch.bind(window);
454
+
455
+ window.fetch = function (input, init) {
456
+ init = init || {};
457
+ var startTime = Date.now();
458
+ // Handle string, Request object, or URL object
459
+ var url = typeof input === "string"
460
+ ? input
461
+ : (input && (input.url || input.href || String(input))) || "";
462
+ var method = init.method || (input && input.method) || "GET";
463
+
464
+ // Don't intercept internal requests
465
+ if (url.indexOf("/__manus__/") === 0) {
466
+ return originalFetch(input, init);
467
+ }
468
+
469
+ // Safely parse headers (avoid breaking if headers format is invalid)
470
+ var requestHeaders = {};
471
+ try {
472
+ if (init.headers) {
473
+ requestHeaders = Object.fromEntries(new Headers(init.headers).entries());
474
+ }
475
+ } catch (e) {
476
+ requestHeaders = { _parseError: true };
477
+ }
478
+
479
+ var entry = {
480
+ timestamp: startTime,
481
+ type: "fetch",
482
+ method: method.toUpperCase(),
483
+ url: url,
484
+ request: {
485
+ headers: requestHeaders,
486
+ body: init.body ? sanitizeValue(tryParseJson(init.body)) : null,
487
+ },
488
+ response: null,
489
+ duration: null,
490
+ error: null,
491
+ };
492
+
493
+ return originalFetch(input, init)
494
+ .then(function (response) {
495
+ entry.duration = Date.now() - startTime;
496
+
497
+ var contentType = (response.headers.get("content-type") || "").toLowerCase();
498
+ var contentLength = response.headers.get("content-length");
499
+
500
+ entry.response = {
501
+ status: response.status,
502
+ statusText: response.statusText,
503
+ headers: Object.fromEntries(response.headers.entries()),
504
+ body: null,
505
+ };
506
+
507
+ // Semantic network hint for agents on failures (sync, no need to wait for body)
508
+ if (response.status >= 400) {
509
+ logUiEvent("network_error", {
510
+ kind: "fetch",
511
+ method: entry.method,
512
+ url: entry.url,
513
+ status: response.status,
514
+ statusText: response.statusText,
515
+ });
516
+ }
517
+
518
+ // Skip body capture for streaming responses (SSE, etc.) to avoid memory leaks
519
+ var isStreaming = contentType.indexOf("text/event-stream") !== -1 ||
520
+ contentType.indexOf("application/stream") !== -1 ||
521
+ contentType.indexOf("application/x-ndjson") !== -1;
522
+ if (isStreaming) {
523
+ entry.response.body = "[Streaming response - not captured]";
524
+ store.networkRequests.push(entry);
525
+ pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
526
+ return response;
527
+ }
528
+
529
+ // Skip body capture for large responses to avoid memory issues
530
+ if (contentLength && parseInt(contentLength, 10) > CONFIG.maxBodyLength) {
531
+ entry.response.body = "[Response too large: " + contentLength + " bytes]";
532
+ store.networkRequests.push(entry);
533
+ pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
534
+ return response;
535
+ }
536
+
537
+ // Skip body capture for binary content types
538
+ var isBinary = contentType.indexOf("image/") !== -1 ||
539
+ contentType.indexOf("video/") !== -1 ||
540
+ contentType.indexOf("audio/") !== -1 ||
541
+ contentType.indexOf("application/octet-stream") !== -1 ||
542
+ contentType.indexOf("application/pdf") !== -1 ||
543
+ contentType.indexOf("application/zip") !== -1;
544
+ if (isBinary) {
545
+ entry.response.body = "[Binary content: " + contentType + "]";
546
+ store.networkRequests.push(entry);
547
+ pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
548
+ return response;
549
+ }
550
+
551
+ // For text responses, clone and read body in background
552
+ var clonedResponse = response.clone();
553
+
554
+ // Async: read body in background, don't block the response
555
+ clonedResponse
556
+ .text()
557
+ .then(function (text) {
558
+ if (text.length <= CONFIG.maxBodyLength) {
559
+ entry.response.body = sanitizeValue(tryParseJson(text));
560
+ } else {
561
+ entry.response.body = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
562
+ }
563
+ })
564
+ .catch(function () {
565
+ entry.response.body = "[Unable to read body]";
566
+ })
567
+ .finally(function () {
568
+ store.networkRequests.push(entry);
569
+ pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
570
+ });
571
+
572
+ // Return response immediately, don't wait for body reading
573
+ return response;
574
+ })
575
+ .catch(function (error) {
576
+ entry.duration = Date.now() - startTime;
577
+ entry.error = { message: error.message, stack: error.stack };
578
+
579
+ store.networkRequests.push(entry);
580
+ pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
581
+
582
+ logUiEvent("network_error", {
583
+ kind: "fetch",
584
+ method: entry.method,
585
+ url: entry.url,
586
+ message: error.message,
587
+ });
588
+
589
+ throw error;
590
+ });
591
+ };
592
+
593
+ // ==========================================================================
594
+ // XHR Interception
595
+ // ==========================================================================
596
+
597
+ var originalXHROpen = XMLHttpRequest.prototype.open;
598
+ var originalXHRSend = XMLHttpRequest.prototype.send;
599
+
600
+ XMLHttpRequest.prototype.open = function (method, url) {
601
+ this._manusData = {
602
+ method: (method || "GET").toUpperCase(),
603
+ url: url,
604
+ startTime: null,
605
+ };
606
+ return originalXHROpen.apply(this, arguments);
607
+ };
608
+
609
+ XMLHttpRequest.prototype.send = function (body) {
610
+ var xhr = this;
611
+
612
+ if (
613
+ xhr._manusData &&
614
+ xhr._manusData.url &&
615
+ xhr._manusData.url.indexOf("/__manus__/") !== 0
616
+ ) {
617
+ xhr._manusData.startTime = Date.now();
618
+ xhr._manusData.requestBody = body ? sanitizeValue(tryParseJson(body)) : null;
619
+
620
+ xhr.addEventListener("load", function () {
621
+ var contentType = (xhr.getResponseHeader("content-type") || "").toLowerCase();
622
+ var responseBody = null;
623
+
624
+ // Skip body capture for streaming responses
625
+ var isStreaming = contentType.indexOf("text/event-stream") !== -1 ||
626
+ contentType.indexOf("application/stream") !== -1 ||
627
+ contentType.indexOf("application/x-ndjson") !== -1;
628
+
629
+ // Skip body capture for binary content types
630
+ var isBinary = contentType.indexOf("image/") !== -1 ||
631
+ contentType.indexOf("video/") !== -1 ||
632
+ contentType.indexOf("audio/") !== -1 ||
633
+ contentType.indexOf("application/octet-stream") !== -1 ||
634
+ contentType.indexOf("application/pdf") !== -1 ||
635
+ contentType.indexOf("application/zip") !== -1;
636
+
637
+ if (isStreaming) {
638
+ responseBody = "[Streaming response - not captured]";
639
+ } else if (isBinary) {
640
+ responseBody = "[Binary content: " + contentType + "]";
641
+ } else {
642
+ // Safe to read responseText for text responses
643
+ try {
644
+ var text = xhr.responseText || "";
645
+ if (text.length > CONFIG.maxBodyLength) {
646
+ responseBody = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
647
+ } else {
648
+ responseBody = sanitizeValue(tryParseJson(text));
649
+ }
650
+ } catch (e) {
651
+ // responseText may throw for non-text responses
652
+ responseBody = "[Unable to read response: " + e.message + "]";
653
+ }
654
+ }
655
+
656
+ var entry = {
657
+ timestamp: xhr._manusData.startTime,
658
+ type: "xhr",
659
+ method: xhr._manusData.method,
660
+ url: xhr._manusData.url,
661
+ request: { body: xhr._manusData.requestBody },
662
+ response: {
663
+ status: xhr.status,
664
+ statusText: xhr.statusText,
665
+ body: responseBody,
666
+ },
667
+ duration: Date.now() - xhr._manusData.startTime,
668
+ error: null,
669
+ };
670
+
671
+ store.networkRequests.push(entry);
672
+ pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
673
+
674
+ if (entry.response && entry.response.status >= 400) {
675
+ logUiEvent("network_error", {
676
+ kind: "xhr",
677
+ method: entry.method,
678
+ url: entry.url,
679
+ status: entry.response.status,
680
+ statusText: entry.response.statusText,
681
+ });
682
+ }
683
+ });
684
+
685
+ xhr.addEventListener("error", function () {
686
+ var entry = {
687
+ timestamp: xhr._manusData.startTime,
688
+ type: "xhr",
689
+ method: xhr._manusData.method,
690
+ url: xhr._manusData.url,
691
+ request: { body: xhr._manusData.requestBody },
692
+ response: null,
693
+ duration: Date.now() - xhr._manusData.startTime,
694
+ error: { message: "Network error" },
695
+ };
696
+
697
+ store.networkRequests.push(entry);
698
+ pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
699
+
700
+ logUiEvent("network_error", {
701
+ kind: "xhr",
702
+ method: entry.method,
703
+ url: entry.url,
704
+ message: "Network error",
705
+ });
706
+ });
707
+ }
708
+
709
+ return originalXHRSend.apply(this, arguments);
710
+ };
711
+
712
+ // ==========================================================================
713
+ // Data Reporting
714
+ // ==========================================================================
715
+
716
+ function reportLogs() {
717
+ var consoleLogs = store.consoleLogs.splice(0);
718
+ var networkRequests = store.networkRequests.splice(0);
719
+ var uiEvents = store.uiEvents.splice(0);
720
+
721
+ // Skip if no new data
722
+ if (
723
+ consoleLogs.length === 0 &&
724
+ networkRequests.length === 0 &&
725
+ uiEvents.length === 0
726
+ ) {
727
+ return Promise.resolve();
728
+ }
729
+
730
+ var payload = {
731
+ timestamp: Date.now(),
732
+ consoleLogs: consoleLogs,
733
+ networkRequests: networkRequests,
734
+ // Mirror uiEvents to sessionEvents for sessionReplay.log
735
+ sessionEvents: uiEvents,
736
+ // agent-friendly semantic events
737
+ uiEvents: uiEvents,
738
+ };
739
+
740
+ return originalFetch(CONFIG.reportEndpoint, {
741
+ method: "POST",
742
+ headers: { "Content-Type": "application/json" },
743
+ body: JSON.stringify(payload),
744
+ }).catch(function () {
745
+ // Put data back on failure (but respect limits)
746
+ store.consoleLogs = consoleLogs.concat(store.consoleLogs);
747
+ store.networkRequests = networkRequests.concat(store.networkRequests);
748
+ store.uiEvents = uiEvents.concat(store.uiEvents);
749
+
750
+ pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
751
+ pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
752
+ pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui);
753
+ });
754
+ }
755
+
756
+ // Periodic reporting
757
+ setInterval(reportLogs, CONFIG.reportInterval);
758
+
759
+ // Report on page unload
760
+ window.addEventListener("beforeunload", function () {
761
+ var consoleLogs = store.consoleLogs;
762
+ var networkRequests = store.networkRequests;
763
+ var uiEvents = store.uiEvents;
764
+
765
+ if (
766
+ consoleLogs.length === 0 &&
767
+ networkRequests.length === 0 &&
768
+ uiEvents.length === 0
769
+ ) {
770
+ return;
771
+ }
772
+
773
+ var payload = {
774
+ timestamp: Date.now(),
775
+ consoleLogs: consoleLogs,
776
+ networkRequests: networkRequests,
777
+ // Mirror uiEvents to sessionEvents for sessionReplay.log
778
+ sessionEvents: uiEvents,
779
+ uiEvents: uiEvents,
780
+ };
781
+
782
+ if (navigator.sendBeacon) {
783
+ var payloadStr = JSON.stringify(payload);
784
+ // sendBeacon has ~64KB limit, truncate if too large
785
+ var MAX_BEACON_SIZE = 60000; // Leave some margin
786
+ if (payloadStr.length > MAX_BEACON_SIZE) {
787
+ // Prioritize: keep recent events, drop older logs
788
+ var truncatedPayload = {
789
+ timestamp: Date.now(),
790
+ consoleLogs: consoleLogs.slice(-50),
791
+ networkRequests: networkRequests.slice(-20),
792
+ sessionEvents: uiEvents.slice(-100),
793
+ uiEvents: uiEvents.slice(-100),
794
+ _truncated: true,
795
+ };
796
+ payloadStr = JSON.stringify(truncatedPayload);
797
+ }
798
+ navigator.sendBeacon(CONFIG.reportEndpoint, payloadStr);
799
+ }
800
+ });
801
+
802
+ // ==========================================================================
803
+ // Initialization
804
+ // ==========================================================================
805
+
806
+ // Install semantic UI listeners ASAP
807
+ try {
808
+ installUiEventListeners();
809
+ } catch (e) {
810
+ console.warn("[Manus] Failed to install UI listeners:", e);
811
+ }
812
+
813
+ // Mark as initialized
814
+ window.__MANUS_DEBUG_COLLECTOR__ = {
815
+ version: "2.0-no-rrweb",
816
+ store: store,
817
+ forceReport: reportLogs,
818
+ };
819
+
820
+ console.debug("[Manus] Debug collector initialized (no rrweb, UI events only)");
821
+ })();
client/src/App.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Toaster } from "@/components/ui/sonner";
2
+ import { TooltipProvider } from "@/components/ui/tooltip";
3
+ import NotFound from "@/pages/NotFound";
4
+ import { Route, Switch } from "wouter";
5
+ import ErrorBoundary from "./components/ErrorBoundary";
6
+ import { ThemeProvider } from "./contexts/ThemeContext";
7
+ import Home from "./pages/Home";
8
+ import Dashboard from "./pages/Dashboard";
9
+ import ChatInterface from "./pages/ChatInterface";
10
+
11
+ function Router() {
12
+ // make sure to consider if you need authentication for certain routes
13
+ return (
14
+ <Switch>
15
+ <Route path="/" component={Home} />
16
+ <Route path="/dashboard" component={Dashboard} />
17
+ <Route path="/chat" component={ChatInterface} />
18
+ <Route path="/404" component={NotFound} />
19
+ {/* Final fallback route */}
20
+ <Route component={NotFound} />
21
+ </Switch>
22
+ );
23
+ }
24
+
25
+ // NOTE: About Theme
26
+ // - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css
27
+ // to keep consistent foreground/background color across components
28
+ // - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook
29
+
30
+ function App() {
31
+ return (
32
+ <ErrorBoundary>
33
+ <ThemeProvider
34
+ defaultTheme="dark"
35
+ // switchable
36
+ >
37
+ <TooltipProvider>
38
+ <Toaster />
39
+ <Router />
40
+ </TooltipProvider>
41
+ </ThemeProvider>
42
+ </ErrorBoundary>
43
+ );
44
+ }
45
+
46
+ export default App;
client/src/_core/hooks/useAuth.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getLoginUrl } from "@/const";
2
+ import { trpc } from "@/lib/trpc";
3
+ import { TRPCClientError } from "@trpc/client";
4
+ import { useCallback, useEffect, useMemo } from "react";
5
+
6
+ type UseAuthOptions = {
7
+ redirectOnUnauthenticated?: boolean;
8
+ redirectPath?: string;
9
+ };
10
+
11
+ export function useAuth(options?: UseAuthOptions) {
12
+ const { redirectOnUnauthenticated = false, redirectPath = getLoginUrl() } =
13
+ options ?? {};
14
+ const utils = trpc.useUtils();
15
+
16
+ const meQuery = trpc.auth.me.useQuery(undefined, {
17
+ retry: false,
18
+ refetchOnWindowFocus: false,
19
+ });
20
+
21
+ const logoutMutation = trpc.auth.logout.useMutation({
22
+ onSuccess: () => {
23
+ utils.auth.me.setData(undefined, null);
24
+ },
25
+ });
26
+
27
+ const logout = useCallback(async () => {
28
+ try {
29
+ await logoutMutation.mutateAsync();
30
+ } catch (error: unknown) {
31
+ if (
32
+ error instanceof TRPCClientError &&
33
+ error.data?.code === "UNAUTHORIZED"
34
+ ) {
35
+ return;
36
+ }
37
+ throw error;
38
+ } finally {
39
+ utils.auth.me.setData(undefined, null);
40
+ await utils.auth.me.invalidate();
41
+ }
42
+ }, [logoutMutation, utils]);
43
+
44
+ const state = useMemo(() => {
45
+ localStorage.setItem(
46
+ "manus-runtime-user-info",
47
+ JSON.stringify(meQuery.data)
48
+ );
49
+ return {
50
+ user: meQuery.data ?? null,
51
+ loading: meQuery.isLoading || logoutMutation.isPending,
52
+ error: meQuery.error ?? logoutMutation.error ?? null,
53
+ isAuthenticated: Boolean(meQuery.data),
54
+ };
55
+ }, [
56
+ meQuery.data,
57
+ meQuery.error,
58
+ meQuery.isLoading,
59
+ logoutMutation.error,
60
+ logoutMutation.isPending,
61
+ ]);
62
+
63
+ useEffect(() => {
64
+ if (!redirectOnUnauthenticated) return;
65
+ if (meQuery.isLoading || logoutMutation.isPending) return;
66
+ if (state.user) return;
67
+ if (typeof window === "undefined") return;
68
+ if (window.location.pathname === redirectPath) return;
69
+
70
+ window.location.href = redirectPath
71
+ }, [
72
+ redirectOnUnauthenticated,
73
+ redirectPath,
74
+ logoutMutation.isPending,
75
+ meQuery.isLoading,
76
+ state.user,
77
+ ]);
78
+
79
+ return {
80
+ ...state,
81
+ refresh: () => meQuery.refetch(),
82
+ logout,
83
+ };
84
+ }
client/src/components/AIChatBox.tsx ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { Textarea } from "@/components/ui/textarea";
3
+ import { ScrollArea } from "@/components/ui/scroll-area";
4
+ import { cn } from "@/lib/utils";
5
+ import { Loader2, Send, User, Sparkles } from "lucide-react";
6
+ import { useState, useEffect, useRef } from "react";
7
+ import { Streamdown } from "streamdown";
8
+
9
+ /**
10
+ * Message type matching server-side LLM Message interface
11
+ */
12
+ export type Message = {
13
+ role: "system" | "user" | "assistant";
14
+ content: string;
15
+ };
16
+
17
+ export type AIChatBoxProps = {
18
+ /**
19
+ * Messages array to display in the chat.
20
+ * Should match the format used by invokeLLM on the server.
21
+ */
22
+ messages: Message[];
23
+
24
+ /**
25
+ * Callback when user sends a message.
26
+ * Typically you'll call a tRPC mutation here to invoke the LLM.
27
+ */
28
+ onSendMessage: (content: string) => void;
29
+
30
+ /**
31
+ * Whether the AI is currently generating a response
32
+ */
33
+ isLoading?: boolean;
34
+
35
+ /**
36
+ * Placeholder text for the input field
37
+ */
38
+ placeholder?: string;
39
+
40
+ /**
41
+ * Custom className for the container
42
+ */
43
+ className?: string;
44
+
45
+ /**
46
+ * Height of the chat box (default: 600px)
47
+ */
48
+ height?: string | number;
49
+
50
+ /**
51
+ * Empty state message to display when no messages
52
+ */
53
+ emptyStateMessage?: string;
54
+
55
+ /**
56
+ * Suggested prompts to display in empty state
57
+ * Click to send directly
58
+ */
59
+ suggestedPrompts?: string[];
60
+ };
61
+
62
+ /**
63
+ * A ready-to-use AI chat box component that integrates with the LLM system.
64
+ *
65
+ * Features:
66
+ * - Matches server-side Message interface for seamless integration
67
+ * - Markdown rendering with Streamdown
68
+ * - Auto-scrolls to latest message
69
+ * - Loading states
70
+ * - Uses global theme colors from index.css
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * const ChatPage = () => {
75
+ * const [messages, setMessages] = useState<Message[]>([
76
+ * { role: "system", content: "You are a helpful assistant." }
77
+ * ]);
78
+ *
79
+ * const chatMutation = trpc.ai.chat.useMutation({
80
+ * onSuccess: (response) => {
81
+ * // Assuming your tRPC endpoint returns the AI response as a string
82
+ * setMessages(prev => [...prev, {
83
+ * role: "assistant",
84
+ * content: response
85
+ * }]);
86
+ * },
87
+ * onError: (error) => {
88
+ * console.error("Chat error:", error);
89
+ * // Optionally show error message to user
90
+ * }
91
+ * });
92
+ *
93
+ * const handleSend = (content: string) => {
94
+ * const newMessages = [...messages, { role: "user", content }];
95
+ * setMessages(newMessages);
96
+ * chatMutation.mutate({ messages: newMessages });
97
+ * };
98
+ *
99
+ * return (
100
+ * <AIChatBox
101
+ * messages={messages}
102
+ * onSendMessage={handleSend}
103
+ * isLoading={chatMutation.isPending}
104
+ * suggestedPrompts={[
105
+ * "Explain quantum computing",
106
+ * "Write a hello world in Python"
107
+ * ]}
108
+ * />
109
+ * );
110
+ * };
111
+ * ```
112
+ */
113
+ export function AIChatBox({
114
+ messages,
115
+ onSendMessage,
116
+ isLoading = false,
117
+ placeholder = "Type your message...",
118
+ className,
119
+ height = "600px",
120
+ emptyStateMessage = "Start a conversation with AI",
121
+ suggestedPrompts,
122
+ }: AIChatBoxProps) {
123
+ const [input, setInput] = useState("");
124
+ const scrollAreaRef = useRef<HTMLDivElement>(null);
125
+ const containerRef = useRef<HTMLDivElement>(null);
126
+ const inputAreaRef = useRef<HTMLFormElement>(null);
127
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
128
+
129
+ // Filter out system messages
130
+ const displayMessages = messages.filter((msg) => msg.role !== "system");
131
+
132
+ // Calculate min-height for last assistant message to push user message to top
133
+ const [minHeightForLastMessage, setMinHeightForLastMessage] = useState(0);
134
+
135
+ useEffect(() => {
136
+ if (containerRef.current && inputAreaRef.current) {
137
+ const containerHeight = containerRef.current.offsetHeight;
138
+ const inputHeight = inputAreaRef.current.offsetHeight;
139
+ const scrollAreaHeight = containerHeight - inputHeight;
140
+
141
+ // Reserve space for:
142
+ // - padding (p-4 = 32px top+bottom)
143
+ // - user message: 40px (item height) + 16px (margin-top from space-y-4) = 56px
144
+ // Note: margin-bottom is not counted because it naturally pushes the assistant message down
145
+ const userMessageReservedHeight = 56;
146
+ const calculatedHeight = scrollAreaHeight - 32 - userMessageReservedHeight;
147
+
148
+ setMinHeightForLastMessage(Math.max(0, calculatedHeight));
149
+ }
150
+ }, []);
151
+
152
+ // Scroll to bottom helper function with smooth animation
153
+ const scrollToBottom = () => {
154
+ const viewport = scrollAreaRef.current?.querySelector(
155
+ '[data-radix-scroll-area-viewport]'
156
+ ) as HTMLDivElement;
157
+
158
+ if (viewport) {
159
+ requestAnimationFrame(() => {
160
+ viewport.scrollTo({
161
+ top: viewport.scrollHeight,
162
+ behavior: 'smooth'
163
+ });
164
+ });
165
+ }
166
+ };
167
+
168
+ const handleSubmit = (e: React.FormEvent) => {
169
+ e.preventDefault();
170
+ const trimmedInput = input.trim();
171
+ if (!trimmedInput || isLoading) return;
172
+
173
+ onSendMessage(trimmedInput);
174
+ setInput("");
175
+
176
+ // Scroll immediately after sending
177
+ scrollToBottom();
178
+
179
+ // Keep focus on input
180
+ textareaRef.current?.focus();
181
+ };
182
+
183
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
184
+ if (e.key === "Enter" && !e.shiftKey) {
185
+ e.preventDefault();
186
+ handleSubmit(e);
187
+ }
188
+ };
189
+
190
+ return (
191
+ <div
192
+ ref={containerRef}
193
+ className={cn(
194
+ "flex flex-col bg-card text-card-foreground rounded-lg border shadow-sm",
195
+ className
196
+ )}
197
+ style={{ height }}
198
+ >
199
+ {/* Messages Area */}
200
+ <div ref={scrollAreaRef} className="flex-1 overflow-hidden">
201
+ {displayMessages.length === 0 ? (
202
+ <div className="flex h-full flex-col p-4">
203
+ <div className="flex flex-1 flex-col items-center justify-center gap-6 text-muted-foreground">
204
+ <div className="flex flex-col items-center gap-3">
205
+ <Sparkles className="size-12 opacity-20" />
206
+ <p className="text-sm">{emptyStateMessage}</p>
207
+ </div>
208
+
209
+ {suggestedPrompts && suggestedPrompts.length > 0 && (
210
+ <div className="flex max-w-2xl flex-wrap justify-center gap-2">
211
+ {suggestedPrompts.map((prompt, index) => (
212
+ <button
213
+ key={index}
214
+ onClick={() => onSendMessage(prompt)}
215
+ disabled={isLoading}
216
+ className="rounded-lg border border-border bg-card px-4 py-2 text-sm transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
217
+ >
218
+ {prompt}
219
+ </button>
220
+ ))}
221
+ </div>
222
+ )}
223
+ </div>
224
+ </div>
225
+ ) : (
226
+ <ScrollArea className="h-full">
227
+ <div className="flex flex-col space-y-4 p-4">
228
+ {displayMessages.map((message, index) => {
229
+ // Apply min-height to last message only if NOT loading (when loading, the loading indicator gets it)
230
+ const isLastMessage = index === displayMessages.length - 1;
231
+ const shouldApplyMinHeight =
232
+ isLastMessage && !isLoading && minHeightForLastMessage > 0;
233
+
234
+ return (
235
+ <div
236
+ key={index}
237
+ className={cn(
238
+ "flex gap-3",
239
+ message.role === "user"
240
+ ? "justify-end items-start"
241
+ : "justify-start items-start"
242
+ )}
243
+ style={
244
+ shouldApplyMinHeight
245
+ ? { minHeight: `${minHeightForLastMessage}px` }
246
+ : undefined
247
+ }
248
+ >
249
+ {message.role === "assistant" && (
250
+ <div className="size-8 shrink-0 mt-1 rounded-full bg-primary/10 flex items-center justify-center">
251
+ <Sparkles className="size-4 text-primary" />
252
+ </div>
253
+ )}
254
+
255
+ <div
256
+ className={cn(
257
+ "max-w-[80%] rounded-lg px-4 py-2.5",
258
+ message.role === "user"
259
+ ? "bg-primary text-primary-foreground"
260
+ : "bg-muted text-foreground"
261
+ )}
262
+ >
263
+ {message.role === "assistant" ? (
264
+ <div className="prose prose-sm dark:prose-invert max-w-none">
265
+ <Streamdown>{message.content}</Streamdown>
266
+ </div>
267
+ ) : (
268
+ <p className="whitespace-pre-wrap text-sm">
269
+ {message.content}
270
+ </p>
271
+ )}
272
+ </div>
273
+
274
+ {message.role === "user" && (
275
+ <div className="size-8 shrink-0 mt-1 rounded-full bg-secondary flex items-center justify-center">
276
+ <User className="size-4 text-secondary-foreground" />
277
+ </div>
278
+ )}
279
+ </div>
280
+ );
281
+ })}
282
+
283
+ {isLoading && (
284
+ <div
285
+ className="flex items-start gap-3"
286
+ style={
287
+ minHeightForLastMessage > 0
288
+ ? { minHeight: `${minHeightForLastMessage}px` }
289
+ : undefined
290
+ }
291
+ >
292
+ <div className="size-8 shrink-0 mt-1 rounded-full bg-primary/10 flex items-center justify-center">
293
+ <Sparkles className="size-4 text-primary" />
294
+ </div>
295
+ <div className="rounded-lg bg-muted px-4 py-2.5">
296
+ <Loader2 className="size-4 animate-spin text-muted-foreground" />
297
+ </div>
298
+ </div>
299
+ )}
300
+ </div>
301
+ </ScrollArea>
302
+ )}
303
+ </div>
304
+
305
+ {/* Input Area */}
306
+ <form
307
+ ref={inputAreaRef}
308
+ onSubmit={handleSubmit}
309
+ className="flex gap-2 p-4 border-t bg-background/50 items-end"
310
+ >
311
+ <Textarea
312
+ ref={textareaRef}
313
+ value={input}
314
+ onChange={(e) => setInput(e.target.value)}
315
+ onKeyDown={handleKeyDown}
316
+ placeholder={placeholder}
317
+ className="flex-1 max-h-32 resize-none min-h-9"
318
+ rows={1}
319
+ />
320
+ <Button
321
+ type="submit"
322
+ size="icon"
323
+ disabled={!input.trim() || isLoading}
324
+ className="shrink-0 h-[38px] w-[38px]"
325
+ >
326
+ {isLoading ? (
327
+ <Loader2 className="size-4 animate-spin" />
328
+ ) : (
329
+ <Send className="size-4" />
330
+ )}
331
+ </Button>
332
+ </form>
333
+ </div>
334
+ );
335
+ }
client/src/components/CodeEditor.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Copy, Download, Play } from "lucide-react";
5
+ import { toast } from "sonner";
6
+
7
+ interface CodeEditorProps {
8
+ code: string;
9
+ language?: string;
10
+ onChange?: (code: string) => void;
11
+ onRun?: () => void;
12
+ }
13
+
14
+ export function CodeEditor({ code, language = "python", onChange, onRun }: CodeEditorProps) {
15
+ const copyToClipboard = () => {
16
+ navigator.clipboard.writeText(code);
17
+ toast.success("Code copied to clipboard!");
18
+ };
19
+
20
+ return (
21
+ <Card className="bg-gray-900 border-green-500/20 overflow-hidden">
22
+ <CardHeader className="bg-black/40 border-b border-green-500/10 py-2 px-4 flex flex-row justify-between items-center">
23
+ <CardTitle className="text-xs font-mono text-gray-400 uppercase tracking-widest">
24
+ {language} Editor
25
+ </CardTitle>
26
+ <div className="flex gap-2">
27
+ <Button variant="ghost" size="icon" className="h-8 w-8 text-gray-400 hover:text-green-400" onClick={copyToClipboard}>
28
+ <Copy size={14} />
29
+ </Button>
30
+ <Button variant="ghost" size="icon" className="h-8 w-8 text-gray-400 hover:text-cyan-400">
31
+ <Download size={14} />
32
+ </Button>
33
+ {onRun && (
34
+ <Button variant="ghost" size="icon" className="h-8 w-8 text-green-500 hover:text-green-400" onClick={onRun}>
35
+ <Play size={14} />
36
+ </Button>
37
+ )}
38
+ </div>
39
+ </CardHeader>
40
+ <CardContent className="p-0">
41
+ <textarea
42
+ value={code}
43
+ onChange={(e) => onChange?.(e.target.value)}
44
+ className="w-full h-96 bg-black text-green-400 font-mono text-sm p-4 focus:outline-none resize-none"
45
+ spellCheck={false}
46
+ />
47
+ </CardContent>
48
+ </Card>
49
+ );
50
+ }
client/src/components/DashboardLayout.tsx ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useAuth } from "@/_core/hooks/useAuth";
2
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuTrigger,
8
+ } from "@/components/ui/dropdown-menu";
9
+ import {
10
+ Sidebar,
11
+ SidebarContent,
12
+ SidebarFooter,
13
+ SidebarHeader,
14
+ SidebarInset,
15
+ SidebarMenu,
16
+ SidebarMenuButton,
17
+ SidebarMenuItem,
18
+ SidebarProvider,
19
+ SidebarTrigger,
20
+ useSidebar,
21
+ } from "@/components/ui/sidebar";
22
+ import { getLoginUrl } from "@/const";
23
+ import { useIsMobile } from "@/hooks/useMobile";
24
+ import { LayoutDashboard, LogOut, PanelLeft, Users } from "lucide-react";
25
+ import { CSSProperties, useEffect, useRef, useState } from "react";
26
+ import { useLocation } from "wouter";
27
+ import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
28
+ import { Button } from "./ui/button";
29
+
30
+ const menuItems = [
31
+ { icon: LayoutDashboard, label: "Page 1", path: "/" },
32
+ { icon: Users, label: "Page 2", path: "/some-path" },
33
+ ];
34
+
35
+ const SIDEBAR_WIDTH_KEY = "sidebar-width";
36
+ const DEFAULT_WIDTH = 280;
37
+ const MIN_WIDTH = 200;
38
+ const MAX_WIDTH = 480;
39
+
40
+ export default function DashboardLayout({
41
+ children,
42
+ }: {
43
+ children: React.ReactNode;
44
+ }) {
45
+ const [sidebarWidth, setSidebarWidth] = useState(() => {
46
+ const saved = localStorage.getItem(SIDEBAR_WIDTH_KEY);
47
+ return saved ? parseInt(saved, 10) : DEFAULT_WIDTH;
48
+ });
49
+ const { loading, user } = useAuth();
50
+
51
+ useEffect(() => {
52
+ localStorage.setItem(SIDEBAR_WIDTH_KEY, sidebarWidth.toString());
53
+ }, [sidebarWidth]);
54
+
55
+ if (loading) {
56
+ return <DashboardLayoutSkeleton />
57
+ }
58
+
59
+ if (!user) {
60
+ return (
61
+ <div className="flex items-center justify-center min-h-screen">
62
+ <div className="flex flex-col items-center gap-8 p-8 max-w-md w-full">
63
+ <div className="flex flex-col items-center gap-6">
64
+ <h1 className="text-2xl font-semibold tracking-tight text-center">
65
+ Sign in to continue
66
+ </h1>
67
+ <p className="text-sm text-muted-foreground text-center max-w-sm">
68
+ Access to this dashboard requires authentication. Continue to launch the login flow.
69
+ </p>
70
+ </div>
71
+ <Button
72
+ onClick={() => {
73
+ window.location.href = getLoginUrl();
74
+ }}
75
+ size="lg"
76
+ className="w-full shadow-lg hover:shadow-xl transition-all"
77
+ >
78
+ Sign in
79
+ </Button>
80
+ </div>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ return (
86
+ <SidebarProvider
87
+ style={
88
+ {
89
+ "--sidebar-width": `${sidebarWidth}px`,
90
+ } as CSSProperties
91
+ }
92
+ >
93
+ <DashboardLayoutContent setSidebarWidth={setSidebarWidth}>
94
+ {children}
95
+ </DashboardLayoutContent>
96
+ </SidebarProvider>
97
+ );
98
+ }
99
+
100
+ type DashboardLayoutContentProps = {
101
+ children: React.ReactNode;
102
+ setSidebarWidth: (width: number) => void;
103
+ };
104
+
105
+ function DashboardLayoutContent({
106
+ children,
107
+ setSidebarWidth,
108
+ }: DashboardLayoutContentProps) {
109
+ const { user, logout } = useAuth();
110
+ const [location, setLocation] = useLocation();
111
+ const { state, toggleSidebar } = useSidebar();
112
+ const isCollapsed = state === "collapsed";
113
+ const [isResizing, setIsResizing] = useState(false);
114
+ const sidebarRef = useRef<HTMLDivElement>(null);
115
+ const activeMenuItem = menuItems.find(item => item.path === location);
116
+ const isMobile = useIsMobile();
117
+
118
+ useEffect(() => {
119
+ if (isCollapsed) {
120
+ setIsResizing(false);
121
+ }
122
+ }, [isCollapsed]);
123
+
124
+ useEffect(() => {
125
+ const handleMouseMove = (e: MouseEvent) => {
126
+ if (!isResizing) return;
127
+
128
+ const sidebarLeft = sidebarRef.current?.getBoundingClientRect().left ?? 0;
129
+ const newWidth = e.clientX - sidebarLeft;
130
+ if (newWidth >= MIN_WIDTH && newWidth <= MAX_WIDTH) {
131
+ setSidebarWidth(newWidth);
132
+ }
133
+ };
134
+
135
+ const handleMouseUp = () => {
136
+ setIsResizing(false);
137
+ };
138
+
139
+ if (isResizing) {
140
+ document.addEventListener("mousemove", handleMouseMove);
141
+ document.addEventListener("mouseup", handleMouseUp);
142
+ document.body.style.cursor = "col-resize";
143
+ document.body.style.userSelect = "none";
144
+ }
145
+
146
+ return () => {
147
+ document.removeEventListener("mousemove", handleMouseMove);
148
+ document.removeEventListener("mouseup", handleMouseUp);
149
+ document.body.style.cursor = "";
150
+ document.body.style.userSelect = "";
151
+ };
152
+ }, [isResizing, setSidebarWidth]);
153
+
154
+ return (
155
+ <>
156
+ <div className="relative" ref={sidebarRef}>
157
+ <Sidebar
158
+ collapsible="icon"
159
+ className="border-r-0"
160
+ disableTransition={isResizing}
161
+ >
162
+ <SidebarHeader className="h-16 justify-center">
163
+ <div className="flex items-center gap-3 px-2 transition-all w-full">
164
+ <button
165
+ onClick={toggleSidebar}
166
+ className="h-8 w-8 flex items-center justify-center hover:bg-accent rounded-lg transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0"
167
+ aria-label="Toggle navigation"
168
+ >
169
+ <PanelLeft className="h-4 w-4 text-muted-foreground" />
170
+ </button>
171
+ {!isCollapsed ? (
172
+ <div className="flex items-center gap-2 min-w-0">
173
+ <span className="font-semibold tracking-tight truncate">
174
+ Navigation
175
+ </span>
176
+ </div>
177
+ ) : null}
178
+ </div>
179
+ </SidebarHeader>
180
+
181
+ <SidebarContent className="gap-0">
182
+ <SidebarMenu className="px-2 py-1">
183
+ {menuItems.map(item => {
184
+ const isActive = location === item.path;
185
+ return (
186
+ <SidebarMenuItem key={item.path}>
187
+ <SidebarMenuButton
188
+ isActive={isActive}
189
+ onClick={() => setLocation(item.path)}
190
+ tooltip={item.label}
191
+ className={`h-10 transition-all font-normal`}
192
+ >
193
+ <item.icon
194
+ className={`h-4 w-4 ${isActive ? "text-primary" : ""}`}
195
+ />
196
+ <span>{item.label}</span>
197
+ </SidebarMenuButton>
198
+ </SidebarMenuItem>
199
+ );
200
+ })}
201
+ </SidebarMenu>
202
+ </SidebarContent>
203
+
204
+ <SidebarFooter className="p-3">
205
+ <DropdownMenu>
206
+ <DropdownMenuTrigger asChild>
207
+ <button className="flex items-center gap-3 rounded-lg px-1 py-1 hover:bg-accent/50 transition-colors w-full text-left group-data-[collapsible=icon]:justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-ring">
208
+ <Avatar className="h-9 w-9 border shrink-0">
209
+ <AvatarFallback className="text-xs font-medium">
210
+ {user?.name?.charAt(0).toUpperCase()}
211
+ </AvatarFallback>
212
+ </Avatar>
213
+ <div className="flex-1 min-w-0 group-data-[collapsible=icon]:hidden">
214
+ <p className="text-sm font-medium truncate leading-none">
215
+ {user?.name || "-"}
216
+ </p>
217
+ <p className="text-xs text-muted-foreground truncate mt-1.5">
218
+ {user?.email || "-"}
219
+ </p>
220
+ </div>
221
+ </button>
222
+ </DropdownMenuTrigger>
223
+ <DropdownMenuContent align="end" className="w-48">
224
+ <DropdownMenuItem
225
+ onClick={logout}
226
+ className="cursor-pointer text-destructive focus:text-destructive"
227
+ >
228
+ <LogOut className="mr-2 h-4 w-4" />
229
+ <span>Sign out</span>
230
+ </DropdownMenuItem>
231
+ </DropdownMenuContent>
232
+ </DropdownMenu>
233
+ </SidebarFooter>
234
+ </Sidebar>
235
+ <div
236
+ className={`absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-primary/20 transition-colors ${isCollapsed ? "hidden" : ""}`}
237
+ onMouseDown={() => {
238
+ if (isCollapsed) return;
239
+ setIsResizing(true);
240
+ }}
241
+ style={{ zIndex: 50 }}
242
+ />
243
+ </div>
244
+
245
+ <SidebarInset>
246
+ {isMobile && (
247
+ <div className="flex border-b h-14 items-center justify-between bg-background/95 px-2 backdrop-blur supports-[backdrop-filter]:backdrop-blur sticky top-0 z-40">
248
+ <div className="flex items-center gap-2">
249
+ <SidebarTrigger className="h-9 w-9 rounded-lg bg-background" />
250
+ <div className="flex items-center gap-3">
251
+ <div className="flex flex-col gap-1">
252
+ <span className="tracking-tight text-foreground">
253
+ {activeMenuItem?.label ?? "Menu"}
254
+ </span>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ )}
260
+ <main className="flex-1 p-4">{children}</main>
261
+ </SidebarInset>
262
+ </>
263
+ );
264
+ }
client/src/components/DashboardLayoutSkeleton.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Skeleton } from './ui/skeleton';
2
+
3
+ export function DashboardLayoutSkeleton() {
4
+ return (
5
+ <div className="flex min-h-screen bg-background">
6
+ {/* Sidebar skeleton */}
7
+ <div className="w-[280px] border-r border-border bg-background p-4 space-y-6">
8
+ {/* Logo area */}
9
+ <div className="flex items-center gap-3 px-2">
10
+ <Skeleton className="h-8 w-8 rounded-md" />
11
+ <Skeleton className="h-4 w-24" />
12
+ </div>
13
+
14
+ {/* Menu items */}
15
+ <div className="space-y-2 px-2">
16
+ <Skeleton className="h-10 w-full rounded-lg" />
17
+ <Skeleton className="h-10 w-full rounded-lg" />
18
+ <Skeleton className="h-10 w-full rounded-lg" />
19
+ </div>
20
+
21
+ {/* User profile area at bottom */}
22
+ <div className="absolute bottom-4 left-4 right-4">
23
+ <div className="flex items-center gap-3 px-1">
24
+ <Skeleton className="h-9 w-9 rounded-full" />
25
+ <div className="flex-1 space-y-2">
26
+ <Skeleton className="h-3 w-20" />
27
+ <Skeleton className="h-2 w-32" />
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+
33
+ {/* Main content skeleton */}
34
+ <div className="flex-1 p-4 space-y-4">
35
+ {/* Content blocks */}
36
+ <Skeleton className="h-12 w-48 rounded-lg" />
37
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
38
+ <Skeleton className="h-32 rounded-xl" />
39
+ <Skeleton className="h-32 rounded-xl" />
40
+ <Skeleton className="h-32 rounded-xl" />
41
+ </div>
42
+ <Skeleton className="h-64 rounded-xl" />
43
+ </div>
44
+ </div>
45
+ );
46
+ }
client/src/components/ErrorBoundary.tsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils";
2
+ import { AlertTriangle, RotateCcw } from "lucide-react";
3
+ import { Component, ReactNode } from "react";
4
+
5
+ interface Props {
6
+ children: ReactNode;
7
+ }
8
+
9
+ interface State {
10
+ hasError: boolean;
11
+ error: Error | null;
12
+ }
13
+
14
+ class ErrorBoundary extends Component<Props, State> {
15
+ constructor(props: Props) {
16
+ super(props);
17
+ this.state = { hasError: false, error: null };
18
+ }
19
+
20
+ static getDerivedStateFromError(error: Error): State {
21
+ return { hasError: true, error };
22
+ }
23
+
24
+ render() {
25
+ if (this.state.hasError) {
26
+ return (
27
+ <div className="flex items-center justify-center min-h-screen p-8 bg-background">
28
+ <div className="flex flex-col items-center w-full max-w-2xl p-8">
29
+ <AlertTriangle
30
+ size={48}
31
+ className="text-destructive mb-6 flex-shrink-0"
32
+ />
33
+
34
+ <h2 className="text-xl mb-4">An unexpected error occurred.</h2>
35
+
36
+ <div className="p-4 w-full rounded bg-muted overflow-auto mb-6">
37
+ <pre className="text-sm text-muted-foreground whitespace-break-spaces">
38
+ {this.state.error?.stack}
39
+ </pre>
40
+ </div>
41
+
42
+ <button
43
+ onClick={() => window.location.reload()}
44
+ className={cn(
45
+ "flex items-center gap-2 px-4 py-2 rounded-lg",
46
+ "bg-primary text-primary-foreground",
47
+ "hover:opacity-90 cursor-pointer"
48
+ )}
49
+ >
50
+ <RotateCcw size={16} />
51
+ Reload Page
52
+ </button>
53
+ </div>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ return this.props.children;
59
+ }
60
+ }
61
+
62
+ export default ErrorBoundary;
client/src/components/IterationChart.tsx ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import {
3
+ Chart as ChartJS,
4
+ CategoryScale,
5
+ LinearScale,
6
+ PointElement,
7
+ LineElement,
8
+ Title,
9
+ Tooltip,
10
+ Legend,
11
+ Filler
12
+ } from 'chart.js';
13
+ import { Line } from 'react-chartjs-2';
14
+
15
+ ChartJS.register(
16
+ CategoryScale,
17
+ LinearScale,
18
+ PointElement,
19
+ LineElement,
20
+ Title,
21
+ Tooltip,
22
+ Legend,
23
+ Filler
24
+ );
25
+
26
+ interface IterationChartProps {
27
+ data: { version: number; score: number }[];
28
+ }
29
+
30
+ export function IterationChart({ data }: IterationChartProps) {
31
+ const chartData = {
32
+ labels: data.map(d => `v${d.version}`),
33
+ datasets: [
34
+ {
35
+ label: 'Score Progress',
36
+ data: data.map(d => d.score),
37
+ borderColor: '#00d9ff',
38
+ backgroundColor: 'rgba(0, 217, 255, 0.1)',
39
+ fill: true,
40
+ tension: 0.4,
41
+ pointBackgroundColor: '#00ff41',
42
+ pointBorderColor: '#fff',
43
+ pointHoverRadius: 6,
44
+ },
45
+ ],
46
+ };
47
+
48
+ const options = {
49
+ responsive: true,
50
+ maintainAspectRatio: false,
51
+ plugins: {
52
+ legend: {
53
+ display: false,
54
+ },
55
+ tooltip: {
56
+ backgroundColor: '#1a1a1a',
57
+ titleColor: '#00d9ff',
58
+ bodyColor: '#fff',
59
+ borderColor: '#00d9ff',
60
+ borderWidth: 1,
61
+ },
62
+ },
63
+ scales: {
64
+ y: {
65
+ min: 0,
66
+ max: 100,
67
+ grid: {
68
+ color: 'rgba(255, 255, 255, 0.1)',
69
+ },
70
+ ticks: {
71
+ color: '#888',
72
+ },
73
+ },
74
+ x: {
75
+ grid: {
76
+ display: false,
77
+ },
78
+ ticks: {
79
+ color: '#888',
80
+ },
81
+ },
82
+ },
83
+ };
84
+
85
+ return (
86
+ <div className="h-64 w-full">
87
+ <Line data={chartData} options={options} />
88
+ </div>
89
+ );
90
+ }
client/src/components/ManusDialog.tsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogTitle,
10
+ } from "@/components/ui/dialog";
11
+
12
+ interface ManusDialogProps {
13
+ title?: string;
14
+ logo?: string;
15
+ open?: boolean;
16
+ onLogin: () => void;
17
+ onOpenChange?: (open: boolean) => void;
18
+ onClose?: () => void;
19
+ }
20
+
21
+ export function ManusDialog({
22
+ title,
23
+ logo,
24
+ open = false,
25
+ onLogin,
26
+ onOpenChange,
27
+ onClose,
28
+ }: ManusDialogProps) {
29
+ const [internalOpen, setInternalOpen] = useState(open);
30
+
31
+ useEffect(() => {
32
+ if (!onOpenChange) {
33
+ setInternalOpen(open);
34
+ }
35
+ }, [open, onOpenChange]);
36
+
37
+ const handleOpenChange = (nextOpen: boolean) => {
38
+ if (onOpenChange) {
39
+ onOpenChange(nextOpen);
40
+ } else {
41
+ setInternalOpen(nextOpen);
42
+ }
43
+
44
+ if (!nextOpen) {
45
+ onClose?.();
46
+ }
47
+ };
48
+
49
+ return (
50
+ <Dialog
51
+ open={onOpenChange ? open : internalOpen}
52
+ onOpenChange={handleOpenChange}
53
+ >
54
+ <DialogContent className="py-5 bg-[#f8f8f7] rounded-[20px] w-[400px] shadow-[0px_4px_11px_0px_rgba(0,0,0,0.08)] border border-[rgba(0,0,0,0.08)] backdrop-blur-2xl p-0 gap-0 text-center">
55
+ <div className="flex flex-col items-center gap-2 p-5 pt-12">
56
+ {logo ? (
57
+ <div className="w-16 h-16 bg-white rounded-xl border border-[rgba(0,0,0,0.08)] flex items-center justify-center">
58
+ <img
59
+ src={logo}
60
+ alt="Dialog graphic"
61
+ className="w-10 h-10 rounded-md"
62
+ />
63
+ </div>
64
+ ) : null}
65
+
66
+ {/* Title and subtitle */}
67
+ {title ? (
68
+ <DialogTitle className="text-xl font-semibold text-[#34322d] leading-[26px] tracking-[-0.44px]">
69
+ {title}
70
+ </DialogTitle>
71
+ ) : null}
72
+ <DialogDescription className="text-sm text-[#858481] leading-5 tracking-[-0.154px]">
73
+ Please login with Manus to continue
74
+ </DialogDescription>
75
+ </div>
76
+
77
+ <DialogFooter className="px-5 py-5">
78
+ {/* Login button */}
79
+ <Button
80
+ onClick={onLogin}
81
+ className="w-full h-10 bg-[#1a1a19] hover:bg-[#1a1a19]/90 text-white rounded-[10px] text-sm font-medium leading-5 tracking-[-0.154px]"
82
+ >
83
+ Login with Manus
84
+ </Button>
85
+ </DialogFooter>
86
+ </DialogContent>
87
+ </Dialog>
88
+ );
89
+ }
client/src/components/Map.tsx ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * GOOGLE MAPS FRONTEND INTEGRATION - ESSENTIAL GUIDE
3
+ *
4
+ * USAGE FROM PARENT COMPONENT:
5
+ * ======
6
+ *
7
+ * const mapRef = useRef<google.maps.Map | null>(null);
8
+ *
9
+ * <MapView
10
+ * initialCenter={{ lat: 40.7128, lng: -74.0060 }}
11
+ * initialZoom={15}
12
+ * onMapReady={(map) => {
13
+ * mapRef.current = map; // Store to control map from parent anytime, google map itself is in charge of the re-rendering, not react state.
14
+ * </MapView>
15
+ *
16
+ * ======
17
+ * Available Libraries and Core Features:
18
+ * -------------------------------
19
+ * 📍 MARKER (from `marker` library)
20
+ * - Attaches to map using { map, position }
21
+ * new google.maps.marker.AdvancedMarkerElement({
22
+ * map,
23
+ * position: { lat: 37.7749, lng: -122.4194 },
24
+ * title: "San Francisco",
25
+ * });
26
+ *
27
+ * -------------------------------
28
+ * 🏢 PLACES (from `places` library)
29
+ * - Does not attach directly to map; use data with your map manually.
30
+ * const place = new google.maps.places.Place({ id: PLACE_ID });
31
+ * await place.fetchFields({ fields: ["displayName", "location"] });
32
+ * map.setCenter(place.location);
33
+ * new google.maps.marker.AdvancedMarkerElement({ map, position: place.location });
34
+ *
35
+ * -------------------------------
36
+ * 🧭 GEOCODER (from `geocoding` library)
37
+ * - Standalone service; manually apply results to map.
38
+ * const geocoder = new google.maps.Geocoder();
39
+ * geocoder.geocode({ address: "New York" }, (results, status) => {
40
+ * if (status === "OK" && results[0]) {
41
+ * map.setCenter(results[0].geometry.location);
42
+ * new google.maps.marker.AdvancedMarkerElement({
43
+ * map,
44
+ * position: results[0].geometry.location,
45
+ * });
46
+ * }
47
+ * });
48
+ *
49
+ * -------------------------------
50
+ * 📐 GEOMETRY (from `geometry` library)
51
+ * - Pure utility functions; not attached to map.
52
+ * const dist = google.maps.geometry.spherical.computeDistanceBetween(p1, p2);
53
+ *
54
+ * -------------------------------
55
+ * 🛣️ ROUTES (from `routes` library)
56
+ * - Combines DirectionsService (standalone) + DirectionsRenderer (map-attached)
57
+ * const directionsService = new google.maps.DirectionsService();
58
+ * const directionsRenderer = new google.maps.DirectionsRenderer({ map });
59
+ * directionsService.route(
60
+ * { origin, destination, travelMode: "DRIVING" },
61
+ * (res, status) => status === "OK" && directionsRenderer.setDirections(res)
62
+ * );
63
+ *
64
+ * -------------------------------
65
+ * 🌦️ MAP LAYERS (attach directly to map)
66
+ * - new google.maps.TrafficLayer().setMap(map);
67
+ * - new google.maps.TransitLayer().setMap(map);
68
+ * - new google.maps.BicyclingLayer().setMap(map);
69
+ *
70
+ * -------------------------------
71
+ * ✅ SUMMARY
72
+ * - “map-attached” → AdvancedMarkerElement, DirectionsRenderer, Layers.
73
+ * - “standalone” → Geocoder, DirectionsService, DistanceMatrixService, ElevationService.
74
+ * - “data-only” → Place, Geometry utilities.
75
+ */
76
+
77
+ /// <reference types="@types/google.maps" />
78
+
79
+ import { useEffect, useRef } from "react";
80
+ import { usePersistFn } from "@/hooks/usePersistFn";
81
+ import { cn } from "@/lib/utils";
82
+
83
+ declare global {
84
+ interface Window {
85
+ google?: typeof google;
86
+ }
87
+ }
88
+
89
+ const API_KEY = import.meta.env.VITE_FRONTEND_FORGE_API_KEY;
90
+ const FORGE_BASE_URL =
91
+ import.meta.env.VITE_FRONTEND_FORGE_API_URL ||
92
+ "https://forge.butterfly-effect.dev";
93
+ const MAPS_PROXY_URL = `${FORGE_BASE_URL}/v1/maps/proxy`;
94
+
95
+ function loadMapScript() {
96
+ return new Promise(resolve => {
97
+ const script = document.createElement("script");
98
+ script.src = `${MAPS_PROXY_URL}/maps/api/js?key=${API_KEY}&v=weekly&libraries=marker,places,geocoding,geometry`;
99
+ script.async = true;
100
+ script.crossOrigin = "anonymous";
101
+ script.onload = () => {
102
+ resolve(null);
103
+ script.remove(); // Clean up immediately
104
+ };
105
+ script.onerror = () => {
106
+ console.error("Failed to load Google Maps script");
107
+ };
108
+ document.head.appendChild(script);
109
+ });
110
+ }
111
+
112
+ interface MapViewProps {
113
+ className?: string;
114
+ initialCenter?: google.maps.LatLngLiteral;
115
+ initialZoom?: number;
116
+ onMapReady?: (map: google.maps.Map) => void;
117
+ }
118
+
119
+ export function MapView({
120
+ className,
121
+ initialCenter = { lat: 37.7749, lng: -122.4194 },
122
+ initialZoom = 12,
123
+ onMapReady,
124
+ }: MapViewProps) {
125
+ const mapContainer = useRef<HTMLDivElement>(null);
126
+ const map = useRef<google.maps.Map | null>(null);
127
+
128
+ const init = usePersistFn(async () => {
129
+ await loadMapScript();
130
+ if (!mapContainer.current) {
131
+ console.error("Map container not found");
132
+ return;
133
+ }
134
+ map.current = new window.google.maps.Map(mapContainer.current, {
135
+ zoom: initialZoom,
136
+ center: initialCenter,
137
+ mapTypeControl: true,
138
+ fullscreenControl: true,
139
+ zoomControl: true,
140
+ streetViewControl: true,
141
+ mapId: "DEMO_MAP_ID",
142
+ });
143
+ if (onMapReady) {
144
+ onMapReady(map.current);
145
+ }
146
+ });
147
+
148
+ useEffect(() => {
149
+ init();
150
+ }, [init]);
151
+
152
+ return (
153
+ <div ref={mapContainer} className={cn("w-full h-[500px]", className)} />
154
+ );
155
+ }
client/src/components/NewProjectDialog.tsx ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import { trpc } from "@/lib/trpc";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Input } from "@/components/ui/input";
5
+ import { Textarea } from "@/components/ui/textarea";
6
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
7
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
8
+ import { toast } from "sonner";
9
+
10
+ interface NewProjectDialogProps {
11
+ onProjectCreated?: () => void;
12
+ }
13
+
14
+ export function NewProjectDialog({ onProjectCreated }: NewProjectDialogProps) {
15
+ const [open, setOpen] = useState(false);
16
+ const [name, setName] = useState("");
17
+ const [description, setDescription] = useState("");
18
+ const [mode, setMode] = useState("auto");
19
+ const [contentType, setContentType] = useState("code");
20
+ const [prompt, setPrompt] = useState("");
21
+
22
+ const createProjectMutation = trpc.projects.create.useMutation({
23
+ onSuccess: () => {
24
+ toast.success("Project created successfully!");
25
+ setOpen(false);
26
+ setName("");
27
+ setDescription("");
28
+ setMode("auto");
29
+ setContentType("code");
30
+ setPrompt("");
31
+ onProjectCreated?.();
32
+ },
33
+ onError: (error) => {
34
+ toast.error("Failed to create project: " + error.message);
35
+ },
36
+ });
37
+
38
+ const handleCreate = async () => {
39
+ if (!name.trim() || !prompt.trim()) {
40
+ toast.error("Please fill in project name and prompt");
41
+ return;
42
+ }
43
+
44
+ await createProjectMutation.mutateAsync({
45
+ name,
46
+ description: description || undefined,
47
+ mode: mode as any,
48
+ contentType: contentType as any,
49
+ originalPrompt: prompt,
50
+ });
51
+ };
52
+
53
+ return (
54
+ <Dialog open={open} onOpenChange={setOpen}>
55
+ <DialogTrigger asChild>
56
+ <Button className="bg-gradient-to-r from-green-500 to-cyan-500 text-black hover:from-green-600 hover:to-cyan-600">
57
+ + New Project
58
+ </Button>
59
+ </DialogTrigger>
60
+ <DialogContent className="bg-gray-900 border-green-500/20 text-white max-w-md">
61
+ <DialogHeader>
62
+ <DialogTitle className="text-green-400">Create New Project</DialogTitle>
63
+ <DialogDescription className="text-gray-400">
64
+ Set up a new AI code generation project
65
+ </DialogDescription>
66
+ </DialogHeader>
67
+
68
+ <div className="space-y-4">
69
+ <div>
70
+ <label className="text-sm text-gray-400 mb-1 block">Project Name</label>
71
+ <Input
72
+ placeholder="My Awesome Project"
73
+ value={name}
74
+ onChange={(e) => setName(e.target.value)}
75
+ className="bg-black/50 border-green-500/20 text-white"
76
+ />
77
+ </div>
78
+
79
+ <div>
80
+ <label className="text-sm text-gray-400 mb-1 block">Description</label>
81
+ <Textarea
82
+ placeholder="What is this project about?"
83
+ value={description}
84
+ onChange={(e) => setDescription(e.target.value)}
85
+ className="bg-black/50 border-green-500/20 text-white h-20"
86
+ />
87
+ </div>
88
+
89
+ <div className="grid grid-cols-2 gap-4">
90
+ <div>
91
+ <label className="text-sm text-gray-400 mb-1 block">Mode</label>
92
+ <Select value={mode} onValueChange={setMode}>
93
+ <SelectTrigger className="bg-black/50 border-green-500/20 text-white">
94
+ <SelectValue />
95
+ </SelectTrigger>
96
+ <SelectContent className="bg-gray-900 border-green-500/20">
97
+ <SelectItem value="auto">Auto</SelectItem>
98
+ <SelectItem value="qwen">Qwen</SelectItem>
99
+ <SelectItem value="deepseek">DeepSeek</SelectItem>
100
+ <SelectItem value="loop">Loop</SelectItem>
101
+ </SelectContent>
102
+ </Select>
103
+ </div>
104
+
105
+ <div>
106
+ <label className="text-sm text-gray-400 mb-1 block">Content Type</label>
107
+ <Select value={contentType} onValueChange={setContentType}>
108
+ <SelectTrigger className="bg-black/50 border-green-500/20 text-white">
109
+ <SelectValue />
110
+ </SelectTrigger>
111
+ <SelectContent className="bg-gray-900 border-green-500/20">
112
+ <SelectItem value="code">Code</SelectItem>
113
+ <SelectItem value="exploit">Exploit</SelectItem>
114
+ <SelectItem value="payload">Payload</SelectItem>
115
+ <SelectItem value="information">Information</SelectItem>
116
+ <SelectItem value="strategy">Strategy</SelectItem>
117
+ </SelectContent>
118
+ </Select>
119
+ </div>
120
+ </div>
121
+
122
+ <div>
123
+ <label className="text-sm text-gray-400 mb-1 block">Initial Prompt</label>
124
+ <Textarea
125
+ placeholder="Describe what you want to generate..."
126
+ value={prompt}
127
+ onChange={(e) => setPrompt(e.target.value)}
128
+ className="bg-black/50 border-green-500/20 text-white h-24"
129
+ />
130
+ </div>
131
+
132
+ <div className="flex gap-2 pt-4">
133
+ <Button
134
+ variant="outline"
135
+ onClick={() => setOpen(false)}
136
+ className="flex-1 border-gray-500/20 text-gray-400"
137
+ >
138
+ Cancel
139
+ </Button>
140
+ <Button
141
+ onClick={handleCreate}
142
+ disabled={createProjectMutation.isPending}
143
+ className="flex-1 bg-gradient-to-r from-green-500 to-cyan-500 text-black hover:from-green-600 hover:to-cyan-600"
144
+ >
145
+ {createProjectMutation.isPending ? "Creating..." : "Create"}
146
+ </Button>
147
+ </div>
148
+ </div>
149
+ </DialogContent>
150
+ </Dialog>
151
+ );
152
+ }
client/src/components/PayloadLibrary.tsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Copy, Download, Shield } from "lucide-react";
7
+ import { toast } from "sonner";
8
+
9
+ interface Payload {
10
+ id: string;
11
+ name: string;
12
+ score: number;
13
+ tags: string[];
14
+ code?: string;
15
+ }
16
+
17
+ interface PayloadLibraryProps {
18
+ payloads: Payload[];
19
+ onSelect?: (payload: Payload) => void;
20
+ }
21
+
22
+ export function PayloadLibrary({ payloads, onSelect }: PayloadLibraryProps) {
23
+ const [searchTerm, setSearchTerm] = useState("");
24
+ const [selectedPayload, setSelectedPayload] = useState<Payload | null>(null);
25
+
26
+ const filtered = payloads.filter(p =>
27
+ p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
28
+ p.tags.some(t => t.toLowerCase().includes(searchTerm.toLowerCase()))
29
+ );
30
+
31
+ return (
32
+ <div className="space-y-4">
33
+ <div className="flex gap-2">
34
+ <Input
35
+ placeholder="Search payloads..."
36
+ value={searchTerm}
37
+ onChange={(e) => setSearchTerm(e.target.value)}
38
+ className="bg-black/50 border-green-500/20 text-white"
39
+ />
40
+ </div>
41
+
42
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
43
+ {filtered.map((payload) => (
44
+ <Card
45
+ key={payload.id}
46
+ className={`bg-gray-900/50 border-green-500/20 cursor-pointer hover:border-green-500/50 transition-all ${
47
+ selectedPayload?.id === payload.id ? "border-green-500 ring-1 ring-green-500" : ""
48
+ }`}
49
+ onClick={() => {
50
+ setSelectedPayload(payload);
51
+ onSelect?.(payload);
52
+ }}
53
+ >
54
+ <CardHeader className="pb-2">
55
+ <div className="flex justify-between items-start">
56
+ <CardTitle className="text-sm text-green-400">{payload.name}</CardTitle>
57
+ <Badge className="bg-cyan-500/20 text-cyan-400 text-xs">{payload.score}/100</Badge>
58
+ </div>
59
+ </CardHeader>
60
+ <CardContent className="space-y-3">
61
+ <div className="flex flex-wrap gap-1">
62
+ {payload.tags.map((tag) => (
63
+ <Badge key={tag} className="bg-purple-500/20 text-purple-300 text-xs">
64
+ {tag}
65
+ </Badge>
66
+ ))}
67
+ </div>
68
+ <div className="flex gap-2">
69
+ <Button
70
+ size="sm"
71
+ variant="ghost"
72
+ className="flex-1 text-xs text-green-400 hover:text-green-300"
73
+ onClick={(e) => {
74
+ e.stopPropagation();
75
+ toast.success("Payload copied!");
76
+ }}
77
+ >
78
+ <Copy size={12} className="mr-1" />
79
+ Copy
80
+ </Button>
81
+ <Button
82
+ size="sm"
83
+ variant="ghost"
84
+ className="flex-1 text-xs text-cyan-400 hover:text-cyan-300"
85
+ onClick={(e) => {
86
+ e.stopPropagation();
87
+ toast.success("Obfuscating...");
88
+ }}
89
+ >
90
+ <Shield size={12} className="mr-1" />
91
+ Obfuscate
92
+ </Button>
93
+ </div>
94
+ </CardContent>
95
+ </Card>
96
+ ))}
97
+ </div>
98
+ </div>
99
+ );
100
+ }
client/src/components/ui/accordion.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
+ import { ChevronDownIcon } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function Accordion({
8
+ ...props
9
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
10
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
11
+ }
12
+
13
+ function AccordionItem({
14
+ className,
15
+ ...props
16
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
17
+ return (
18
+ <AccordionPrimitive.Item
19
+ data-slot="accordion-item"
20
+ className={cn("border-b last:border-b-0", className)}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+
26
+ function AccordionTrigger({
27
+ className,
28
+ children,
29
+ ...props
30
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
31
+ return (
32
+ <AccordionPrimitive.Header className="flex">
33
+ <AccordionPrimitive.Trigger
34
+ data-slot="accordion-trigger"
35
+ className={cn(
36
+ "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ {children}
42
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
43
+ </AccordionPrimitive.Trigger>
44
+ </AccordionPrimitive.Header>
45
+ );
46
+ }
47
+
48
+ function AccordionContent({
49
+ className,
50
+ children,
51
+ ...props
52
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
53
+ return (
54
+ <AccordionPrimitive.Content
55
+ data-slot="accordion-content"
56
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
57
+ {...props}
58
+ >
59
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
60
+ </AccordionPrimitive.Content>
61
+ );
62
+ }
63
+
64
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
client/src/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ function AlertDialog({
8
+ ...props
9
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
10
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
11
+ }
12
+
13
+ function AlertDialogTrigger({
14
+ ...props
15
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
16
+ return (
17
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
18
+ );
19
+ }
20
+
21
+ function AlertDialogPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
24
+ return (
25
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
26
+ );
27
+ }
28
+
29
+ function AlertDialogOverlay({
30
+ className,
31
+ ...props
32
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
33
+ return (
34
+ <AlertDialogPrimitive.Overlay
35
+ data-slot="alert-dialog-overlay"
36
+ className={cn(
37
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ );
43
+ }
44
+
45
+ function AlertDialogContent({
46
+ className,
47
+ ...props
48
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
49
+ return (
50
+ <AlertDialogPortal>
51
+ <AlertDialogOverlay />
52
+ <AlertDialogPrimitive.Content
53
+ data-slot="alert-dialog-content"
54
+ className={cn(
55
+ "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
56
+ className
57
+ )}
58
+ {...props}
59
+ />
60
+ </AlertDialogPortal>
61
+ );
62
+ }
63
+
64
+ function AlertDialogHeader({
65
+ className,
66
+ ...props
67
+ }: React.ComponentProps<"div">) {
68
+ return (
69
+ <div
70
+ data-slot="alert-dialog-header"
71
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
72
+ {...props}
73
+ />
74
+ );
75
+ }
76
+
77
+ function AlertDialogFooter({
78
+ className,
79
+ ...props
80
+ }: React.ComponentProps<"div">) {
81
+ return (
82
+ <div
83
+ data-slot="alert-dialog-footer"
84
+ className={cn(
85
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
86
+ className
87
+ )}
88
+ {...props}
89
+ />
90
+ );
91
+ }
92
+
93
+ function AlertDialogTitle({
94
+ className,
95
+ ...props
96
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
97
+ return (
98
+ <AlertDialogPrimitive.Title
99
+ data-slot="alert-dialog-title"
100
+ className={cn("text-lg font-semibold", className)}
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ function AlertDialogDescription({
107
+ className,
108
+ ...props
109
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
110
+ return (
111
+ <AlertDialogPrimitive.Description
112
+ data-slot="alert-dialog-description"
113
+ className={cn("text-muted-foreground text-sm", className)}
114
+ {...props}
115
+ />
116
+ );
117
+ }
118
+
119
+ function AlertDialogAction({
120
+ className,
121
+ ...props
122
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
123
+ return (
124
+ <AlertDialogPrimitive.Action
125
+ className={cn(buttonVariants(), className)}
126
+ {...props}
127
+ />
128
+ );
129
+ }
130
+
131
+ function AlertDialogCancel({
132
+ className,
133
+ ...props
134
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
135
+ return (
136
+ <AlertDialogPrimitive.Cancel
137
+ className={cn(buttonVariants({ variant: "outline" }), className)}
138
+ {...props}
139
+ />
140
+ );
141
+ }
142
+
143
+ export {
144
+ AlertDialog,
145
+ AlertDialogPortal,
146
+ AlertDialogOverlay,
147
+ AlertDialogTrigger,
148
+ AlertDialogContent,
149
+ AlertDialogHeader,
150
+ AlertDialogFooter,
151
+ AlertDialogTitle,
152
+ AlertDialogDescription,
153
+ AlertDialogAction,
154
+ AlertDialogCancel,
155
+ };
client/src/components/ui/alert.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ );
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ );
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ );
64
+ }
65
+
66
+ export { Alert, AlertTitle, AlertDescription };
client/src/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
2
+
3
+ function AspectRatio({
4
+ ...props
5
+ }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
6
+ return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
7
+ }
8
+
9
+ export { AspectRatio };
client/src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function Avatar({
7
+ className,
8
+ ...props
9
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
10
+ return (
11
+ <AvatarPrimitive.Root
12
+ data-slot="avatar"
13
+ className={cn(
14
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function AvatarImage({
23
+ className,
24
+ ...props
25
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
26
+ return (
27
+ <AvatarPrimitive.Image
28
+ data-slot="avatar-image"
29
+ className={cn("aspect-square size-full", className)}
30
+ {...props}
31
+ />
32
+ );
33
+ }
34
+
35
+ function AvatarFallback({
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
39
+ return (
40
+ <AvatarPrimitive.Fallback
41
+ data-slot="avatar-fallback"
42
+ className={cn(
43
+ "bg-muted flex size-full items-center justify-center rounded-full",
44
+ className
45
+ )}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ export { Avatar, AvatarImage, AvatarFallback };
client/src/components/ui/badge.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
+ );
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span";
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ );
44
+ }
45
+
46
+ export { Badge, badgeVariants };
client/src/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8
+ return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
9
+ }
10
+
11
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12
+ return (
13
+ <ol
14
+ data-slot="breadcrumb-list"
15
+ className={cn(
16
+ "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25
+ return (
26
+ <li
27
+ data-slot="breadcrumb-item"
28
+ className={cn("inline-flex items-center gap-1.5", className)}
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+
34
+ function BreadcrumbLink({
35
+ asChild,
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<"a"> & {
39
+ asChild?: boolean;
40
+ }) {
41
+ const Comp = asChild ? Slot : "a";
42
+
43
+ return (
44
+ <Comp
45
+ data-slot="breadcrumb-link"
46
+ className={cn("hover:text-foreground transition-colors", className)}
47
+ {...props}
48
+ />
49
+ );
50
+ }
51
+
52
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53
+ return (
54
+ <span
55
+ data-slot="breadcrumb-page"
56
+ role="link"
57
+ aria-disabled="true"
58
+ aria-current="page"
59
+ className={cn("text-foreground font-normal", className)}
60
+ {...props}
61
+ />
62
+ );
63
+ }
64
+
65
+ function BreadcrumbSeparator({
66
+ children,
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"li">) {
70
+ return (
71
+ <li
72
+ data-slot="breadcrumb-separator"
73
+ role="presentation"
74
+ aria-hidden="true"
75
+ className={cn("[&>svg]:size-3.5", className)}
76
+ {...props}
77
+ >
78
+ {children ?? <ChevronRight />}
79
+ </li>
80
+ );
81
+ }
82
+
83
+ function BreadcrumbEllipsis({
84
+ className,
85
+ ...props
86
+ }: React.ComponentProps<"span">) {
87
+ return (
88
+ <span
89
+ data-slot="breadcrumb-ellipsis"
90
+ role="presentation"
91
+ aria-hidden="true"
92
+ className={cn("flex size-9 items-center justify-center", className)}
93
+ {...props}
94
+ >
95
+ <MoreHorizontal className="size-4" />
96
+ <span className="sr-only">More</span>
97
+ </span>
98
+ );
99
+ }
100
+
101
+ export {
102
+ Breadcrumb,
103
+ BreadcrumbList,
104
+ BreadcrumbItem,
105
+ BreadcrumbLink,
106
+ BreadcrumbPage,
107
+ BreadcrumbSeparator,
108
+ BreadcrumbEllipsis,
109
+ };
client/src/components/ui/button-group.tsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Slot } from "@radix-ui/react-slot";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+ import { Separator } from "@/components/ui/separator";
6
+
7
+ const buttonGroupVariants = cva(
8
+ "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
9
+ {
10
+ variants: {
11
+ orientation: {
12
+ horizontal:
13
+ "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
14
+ vertical:
15
+ "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ orientation: "horizontal",
20
+ },
21
+ }
22
+ );
23
+
24
+ function ButtonGroup({
25
+ className,
26
+ orientation,
27
+ ...props
28
+ }: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
29
+ return (
30
+ <div
31
+ role="group"
32
+ data-slot="button-group"
33
+ data-orientation={orientation}
34
+ className={cn(buttonGroupVariants({ orientation }), className)}
35
+ {...props}
36
+ />
37
+ );
38
+ }
39
+
40
+ function ButtonGroupText({
41
+ className,
42
+ asChild = false,
43
+ ...props
44
+ }: React.ComponentProps<"div"> & {
45
+ asChild?: boolean;
46
+ }) {
47
+ const Comp = asChild ? Slot : "div";
48
+
49
+ return (
50
+ <Comp
51
+ className={cn(
52
+ "bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
53
+ className
54
+ )}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+
60
+ function ButtonGroupSeparator({
61
+ className,
62
+ orientation = "vertical",
63
+ ...props
64
+ }: React.ComponentProps<typeof Separator>) {
65
+ return (
66
+ <Separator
67
+ data-slot="button-group-separator"
68
+ orientation={orientation}
69
+ className={cn(
70
+ "bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
71
+ className
72
+ )}
73
+ {...props}
74
+ />
75
+ );
76
+ }
77
+
78
+ export {
79
+ ButtonGroup,
80
+ ButtonGroupSeparator,
81
+ ButtonGroupText,
82
+ buttonGroupVariants,
83
+ };
client/src/components/ui/button.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-transparent shadow-xs hover:bg-accent dark:bg-transparent dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ "icon-sm": "size-8",
29
+ "icon-lg": "size-10",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ size: "default",
35
+ },
36
+ }
37
+ );
38
+
39
+ function Button({
40
+ className,
41
+ variant,
42
+ size,
43
+ asChild = false,
44
+ ...props
45
+ }: React.ComponentProps<"button"> &
46
+ VariantProps<typeof buttonVariants> & {
47
+ asChild?: boolean;
48
+ }) {
49
+ const Comp = asChild ? Slot : "button";
50
+
51
+ return (
52
+ <Comp
53
+ data-slot="button"
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+
60
+ export { Button, buttonVariants };
client/src/components/ui/calendar.tsx ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import {
3
+ ChevronDownIcon,
4
+ ChevronLeftIcon,
5
+ ChevronRightIcon,
6
+ } from "lucide-react";
7
+ import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
8
+
9
+ import { cn } from "@/lib/utils";
10
+ import { Button, buttonVariants } from "@/components/ui/button";
11
+
12
+ function Calendar({
13
+ className,
14
+ classNames,
15
+ showOutsideDays = true,
16
+ captionLayout = "label",
17
+ buttonVariant = "ghost",
18
+ formatters,
19
+ components,
20
+ ...props
21
+ }: React.ComponentProps<typeof DayPicker> & {
22
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"];
23
+ }) {
24
+ const defaultClassNames = getDefaultClassNames();
25
+
26
+ return (
27
+ <DayPicker
28
+ showOutsideDays={showOutsideDays}
29
+ className={cn(
30
+ "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
31
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
32
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
33
+ className
34
+ )}
35
+ captionLayout={captionLayout}
36
+ formatters={{
37
+ formatMonthDropdown: date =>
38
+ date.toLocaleString("default", { month: "short" }),
39
+ ...formatters,
40
+ }}
41
+ classNames={{
42
+ root: cn("w-fit", defaultClassNames.root),
43
+ months: cn(
44
+ "flex gap-4 flex-col md:flex-row relative",
45
+ defaultClassNames.months
46
+ ),
47
+ month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
48
+ nav: cn(
49
+ "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
50
+ defaultClassNames.nav
51
+ ),
52
+ button_previous: cn(
53
+ buttonVariants({ variant: buttonVariant }),
54
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
55
+ defaultClassNames.button_previous
56
+ ),
57
+ button_next: cn(
58
+ buttonVariants({ variant: buttonVariant }),
59
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
60
+ defaultClassNames.button_next
61
+ ),
62
+ month_caption: cn(
63
+ "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
64
+ defaultClassNames.month_caption
65
+ ),
66
+ dropdowns: cn(
67
+ "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
68
+ defaultClassNames.dropdowns
69
+ ),
70
+ dropdown_root: cn(
71
+ "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
72
+ defaultClassNames.dropdown_root
73
+ ),
74
+ dropdown: cn(
75
+ "absolute bg-popover inset-0 opacity-0",
76
+ defaultClassNames.dropdown
77
+ ),
78
+ caption_label: cn(
79
+ "select-none font-medium",
80
+ captionLayout === "label"
81
+ ? "text-sm"
82
+ : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
83
+ defaultClassNames.caption_label
84
+ ),
85
+ table: "w-full border-collapse",
86
+ weekdays: cn("flex", defaultClassNames.weekdays),
87
+ weekday: cn(
88
+ "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
89
+ defaultClassNames.weekday
90
+ ),
91
+ week: cn("flex w-full mt-2", defaultClassNames.week),
92
+ week_number_header: cn(
93
+ "select-none w-(--cell-size)",
94
+ defaultClassNames.week_number_header
95
+ ),
96
+ week_number: cn(
97
+ "text-[0.8rem] select-none text-muted-foreground",
98
+ defaultClassNames.week_number
99
+ ),
100
+ day: cn(
101
+ "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
102
+ defaultClassNames.day
103
+ ),
104
+ range_start: cn(
105
+ "rounded-l-md bg-accent",
106
+ defaultClassNames.range_start
107
+ ),
108
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
109
+ range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
110
+ today: cn(
111
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
112
+ defaultClassNames.today
113
+ ),
114
+ outside: cn(
115
+ "text-muted-foreground aria-selected:text-muted-foreground",
116
+ defaultClassNames.outside
117
+ ),
118
+ disabled: cn(
119
+ "text-muted-foreground opacity-50",
120
+ defaultClassNames.disabled
121
+ ),
122
+ hidden: cn("invisible", defaultClassNames.hidden),
123
+ ...classNames,
124
+ }}
125
+ components={{
126
+ Root: ({ className, rootRef, ...props }) => {
127
+ return (
128
+ <div
129
+ data-slot="calendar"
130
+ ref={rootRef}
131
+ className={cn(className)}
132
+ {...props}
133
+ />
134
+ );
135
+ },
136
+ Chevron: ({ className, orientation, ...props }) => {
137
+ if (orientation === "left") {
138
+ return (
139
+ <ChevronLeftIcon className={cn("size-4", className)} {...props} />
140
+ );
141
+ }
142
+
143
+ if (orientation === "right") {
144
+ return (
145
+ <ChevronRightIcon
146
+ className={cn("size-4", className)}
147
+ {...props}
148
+ />
149
+ );
150
+ }
151
+
152
+ return (
153
+ <ChevronDownIcon className={cn("size-4", className)} {...props} />
154
+ );
155
+ },
156
+ DayButton: CalendarDayButton,
157
+ WeekNumber: ({ children, ...props }) => {
158
+ return (
159
+ <td {...props}>
160
+ <div className="flex size-(--cell-size) items-center justify-center text-center">
161
+ {children}
162
+ </div>
163
+ </td>
164
+ );
165
+ },
166
+ ...components,
167
+ }}
168
+ {...props}
169
+ />
170
+ );
171
+ }
172
+
173
+ function CalendarDayButton({
174
+ className,
175
+ day,
176
+ modifiers,
177
+ ...props
178
+ }: React.ComponentProps<typeof DayButton>) {
179
+ const defaultClassNames = getDefaultClassNames();
180
+
181
+ const ref = React.useRef<HTMLButtonElement>(null);
182
+ React.useEffect(() => {
183
+ if (modifiers.focused) ref.current?.focus();
184
+ }, [modifiers.focused]);
185
+
186
+ return (
187
+ <Button
188
+ ref={ref}
189
+ variant="ghost"
190
+ size="icon"
191
+ data-day={day.date.toLocaleDateString()}
192
+ data-selected-single={
193
+ modifiers.selected &&
194
+ !modifiers.range_start &&
195
+ !modifiers.range_end &&
196
+ !modifiers.range_middle
197
+ }
198
+ data-range-start={modifiers.range_start}
199
+ data-range-end={modifiers.range_end}
200
+ data-range-middle={modifiers.range_middle}
201
+ className={cn(
202
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
203
+ defaultClassNames.day,
204
+ className
205
+ )}
206
+ {...props}
207
+ />
208
+ );
209
+ }
210
+
211
+ export { Calendar, CalendarDayButton };
client/src/components/ui/card.tsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ );
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ };
client/src/components/ui/carousel.tsx ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ function Carousel({
44
+ orientation = "horizontal",
45
+ opts,
46
+ setApi,
47
+ plugins,
48
+ className,
49
+ children,
50
+ ...props
51
+ }: React.ComponentProps<"div"> & CarouselProps) {
52
+ const [carouselRef, api] = useEmblaCarousel(
53
+ {
54
+ ...opts,
55
+ axis: orientation === "horizontal" ? "x" : "y",
56
+ },
57
+ plugins
58
+ );
59
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
60
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
61
+
62
+ const onSelect = React.useCallback((api: CarouselApi) => {
63
+ if (!api) return;
64
+ setCanScrollPrev(api.canScrollPrev());
65
+ setCanScrollNext(api.canScrollNext());
66
+ }, []);
67
+
68
+ const scrollPrev = React.useCallback(() => {
69
+ api?.scrollPrev();
70
+ }, [api]);
71
+
72
+ const scrollNext = React.useCallback(() => {
73
+ api?.scrollNext();
74
+ }, [api]);
75
+
76
+ const handleKeyDown = React.useCallback(
77
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
78
+ if (event.key === "ArrowLeft") {
79
+ event.preventDefault();
80
+ scrollPrev();
81
+ } else if (event.key === "ArrowRight") {
82
+ event.preventDefault();
83
+ scrollNext();
84
+ }
85
+ },
86
+ [scrollPrev, scrollNext]
87
+ );
88
+
89
+ React.useEffect(() => {
90
+ if (!api || !setApi) return;
91
+ setApi(api);
92
+ }, [api, setApi]);
93
+
94
+ React.useEffect(() => {
95
+ if (!api) return;
96
+ onSelect(api);
97
+ api.on("reInit", onSelect);
98
+ api.on("select", onSelect);
99
+
100
+ return () => {
101
+ api?.off("select", onSelect);
102
+ };
103
+ }, [api, onSelect]);
104
+
105
+ return (
106
+ <CarouselContext.Provider
107
+ value={{
108
+ carouselRef,
109
+ api: api,
110
+ opts,
111
+ orientation:
112
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
113
+ scrollPrev,
114
+ scrollNext,
115
+ canScrollPrev,
116
+ canScrollNext,
117
+ }}
118
+ >
119
+ <div
120
+ onKeyDownCapture={handleKeyDown}
121
+ className={cn("relative", className)}
122
+ role="region"
123
+ aria-roledescription="carousel"
124
+ data-slot="carousel"
125
+ {...props}
126
+ >
127
+ {children}
128
+ </div>
129
+ </CarouselContext.Provider>
130
+ );
131
+ }
132
+
133
+ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
134
+ const { carouselRef, orientation } = useCarousel();
135
+
136
+ return (
137
+ <div
138
+ ref={carouselRef}
139
+ className="overflow-hidden"
140
+ data-slot="carousel-content"
141
+ >
142
+ <div
143
+ className={cn(
144
+ "flex",
145
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
146
+ className
147
+ )}
148
+ {...props}
149
+ />
150
+ </div>
151
+ );
152
+ }
153
+
154
+ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
155
+ const { orientation } = useCarousel();
156
+
157
+ return (
158
+ <div
159
+ role="group"
160
+ aria-roledescription="slide"
161
+ data-slot="carousel-item"
162
+ className={cn(
163
+ "min-w-0 shrink-0 grow-0 basis-full",
164
+ orientation === "horizontal" ? "pl-4" : "pt-4",
165
+ className
166
+ )}
167
+ {...props}
168
+ />
169
+ );
170
+ }
171
+
172
+ function CarouselPrevious({
173
+ className,
174
+ variant = "outline",
175
+ size = "icon",
176
+ ...props
177
+ }: React.ComponentProps<typeof Button>) {
178
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
179
+
180
+ return (
181
+ <Button
182
+ data-slot="carousel-previous"
183
+ variant={variant}
184
+ size={size}
185
+ className={cn(
186
+ "absolute size-8 rounded-full",
187
+ orientation === "horizontal"
188
+ ? "top-1/2 -left-12 -translate-y-1/2"
189
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
190
+ className
191
+ )}
192
+ disabled={!canScrollPrev}
193
+ onClick={scrollPrev}
194
+ {...props}
195
+ >
196
+ <ArrowLeft />
197
+ <span className="sr-only">Previous slide</span>
198
+ </Button>
199
+ );
200
+ }
201
+
202
+ function CarouselNext({
203
+ className,
204
+ variant = "outline",
205
+ size = "icon",
206
+ ...props
207
+ }: React.ComponentProps<typeof Button>) {
208
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
209
+
210
+ return (
211
+ <Button
212
+ data-slot="carousel-next"
213
+ variant={variant}
214
+ size={size}
215
+ className={cn(
216
+ "absolute size-8 rounded-full",
217
+ orientation === "horizontal"
218
+ ? "top-1/2 -right-12 -translate-y-1/2"
219
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
220
+ className
221
+ )}
222
+ disabled={!canScrollNext}
223
+ onClick={scrollNext}
224
+ {...props}
225
+ >
226
+ <ArrowRight />
227
+ <span className="sr-only">Next slide</span>
228
+ </Button>
229
+ );
230
+ }
231
+
232
+ export {
233
+ type CarouselApi,
234
+ Carousel,
235
+ CarouselContent,
236
+ CarouselItem,
237
+ CarouselPrevious,
238
+ CarouselNext,
239
+ };
client/src/components/ui/chart.tsx ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as RechartsPrimitive from "recharts";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ // Format: { THEME_NAME: CSS_SELECTOR }
7
+ const THEMES = { light: "", dark: ".dark" } as const;
8
+
9
+ export type ChartConfig = {
10
+ [k in string]: {
11
+ label?: React.ReactNode;
12
+ icon?: React.ComponentType;
13
+ } & (
14
+ | { color?: string; theme?: never }
15
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
16
+ );
17
+ };
18
+
19
+ type ChartContextProps = {
20
+ config: ChartConfig;
21
+ };
22
+
23
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
24
+
25
+ function useChart() {
26
+ const context = React.useContext(ChartContext);
27
+
28
+ if (!context) {
29
+ throw new Error("useChart must be used within a <ChartContainer />");
30
+ }
31
+
32
+ return context;
33
+ }
34
+
35
+ function ChartContainer({
36
+ id,
37
+ className,
38
+ children,
39
+ config,
40
+ ...props
41
+ }: React.ComponentProps<"div"> & {
42
+ config: ChartConfig;
43
+ children: React.ComponentProps<
44
+ typeof RechartsPrimitive.ResponsiveContainer
45
+ >["children"];
46
+ }) {
47
+ const uniqueId = React.useId();
48
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
49
+
50
+ return (
51
+ <ChartContext.Provider value={{ config }}>
52
+ <div
53
+ data-slot="chart"
54
+ data-chart={chartId}
55
+ className={cn(
56
+ "[&_.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-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 flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
57
+ className
58
+ )}
59
+ {...props}
60
+ >
61
+ <ChartStyle id={chartId} config={config} />
62
+ <RechartsPrimitive.ResponsiveContainer>
63
+ {children}
64
+ </RechartsPrimitive.ResponsiveContainer>
65
+ </div>
66
+ </ChartContext.Provider>
67
+ );
68
+ }
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
+ function ChartTooltipContent({
106
+ active,
107
+ payload,
108
+ className,
109
+ indicator = "dot",
110
+ hideLabel = false,
111
+ hideIndicator = false,
112
+ label,
113
+ labelFormatter,
114
+ labelClassName,
115
+ formatter,
116
+ color,
117
+ nameKey,
118
+ labelKey,
119
+ }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
120
+ React.ComponentProps<"div"> & {
121
+ hideLabel?: boolean;
122
+ hideIndicator?: boolean;
123
+ indicator?: "line" | "dot" | "dashed";
124
+ nameKey?: string;
125
+ labelKey?: string;
126
+ }) {
127
+ const { config } = useChart();
128
+
129
+ const tooltipLabel = React.useMemo(() => {
130
+ if (hideLabel || !payload?.length) {
131
+ return null;
132
+ }
133
+
134
+ const [item] = payload;
135
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
136
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
137
+ const value =
138
+ !labelKey && typeof label === "string"
139
+ ? config[label as keyof typeof config]?.label || label
140
+ : itemConfig?.label;
141
+
142
+ if (labelFormatter) {
143
+ return (
144
+ <div className={cn("font-medium", labelClassName)}>
145
+ {labelFormatter(value, payload)}
146
+ </div>
147
+ );
148
+ }
149
+
150
+ if (!value) {
151
+ return null;
152
+ }
153
+
154
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
155
+ }, [
156
+ label,
157
+ labelFormatter,
158
+ payload,
159
+ hideLabel,
160
+ labelClassName,
161
+ config,
162
+ labelKey,
163
+ ]);
164
+
165
+ if (!active || !payload?.length) {
166
+ return null;
167
+ }
168
+
169
+ const nestLabel = payload.length === 1 && indicator !== "dot";
170
+
171
+ return (
172
+ <div
173
+ className={cn(
174
+ "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
175
+ className
176
+ )}
177
+ >
178
+ {!nestLabel ? tooltipLabel : null}
179
+ <div className="grid gap-1.5">
180
+ {payload
181
+ .filter(item => item.type !== "none")
182
+ .map((item, index) => {
183
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
184
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
185
+ const indicatorColor = color || item.payload.fill || item.color;
186
+
187
+ return (
188
+ <div
189
+ key={item.dataKey}
190
+ className={cn(
191
+ "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
192
+ indicator === "dot" && "items-center"
193
+ )}
194
+ >
195
+ {formatter && item?.value !== undefined && item.name ? (
196
+ formatter(item.value, item.name, item, index, item.payload)
197
+ ) : (
198
+ <>
199
+ {itemConfig?.icon ? (
200
+ <itemConfig.icon />
201
+ ) : (
202
+ !hideIndicator && (
203
+ <div
204
+ className={cn(
205
+ "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
206
+ {
207
+ "h-2.5 w-2.5": indicator === "dot",
208
+ "w-1": indicator === "line",
209
+ "w-0 border-[1.5px] border-dashed bg-transparent":
210
+ indicator === "dashed",
211
+ "my-0.5": nestLabel && indicator === "dashed",
212
+ }
213
+ )}
214
+ style={
215
+ {
216
+ "--color-bg": indicatorColor,
217
+ "--color-border": indicatorColor,
218
+ } as React.CSSProperties
219
+ }
220
+ />
221
+ )
222
+ )}
223
+ <div
224
+ className={cn(
225
+ "flex flex-1 justify-between leading-none",
226
+ nestLabel ? "items-end" : "items-center"
227
+ )}
228
+ >
229
+ <div className="grid gap-1.5">
230
+ {nestLabel ? tooltipLabel : null}
231
+ <span className="text-muted-foreground">
232
+ {itemConfig?.label || item.name}
233
+ </span>
234
+ </div>
235
+ {item.value && (
236
+ <span className="text-foreground font-mono font-medium tabular-nums">
237
+ {item.value.toLocaleString()}
238
+ </span>
239
+ )}
240
+ </div>
241
+ </>
242
+ )}
243
+ </div>
244
+ );
245
+ })}
246
+ </div>
247
+ </div>
248
+ );
249
+ }
250
+
251
+ const ChartLegend = RechartsPrimitive.Legend;
252
+
253
+ function ChartLegendContent({
254
+ className,
255
+ hideIcon = false,
256
+ payload,
257
+ verticalAlign = "bottom",
258
+ nameKey,
259
+ }: React.ComponentProps<"div"> &
260
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
261
+ hideIcon?: boolean;
262
+ nameKey?: string;
263
+ }) {
264
+ const { config } = useChart();
265
+
266
+ if (!payload?.length) {
267
+ return null;
268
+ }
269
+
270
+ return (
271
+ <div
272
+ className={cn(
273
+ "flex items-center justify-center gap-4",
274
+ verticalAlign === "top" ? "pb-3" : "pt-3",
275
+ className
276
+ )}
277
+ >
278
+ {payload
279
+ .filter(item => item.type !== "none")
280
+ .map(item => {
281
+ const key = `${nameKey || item.dataKey || "value"}`;
282
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
283
+
284
+ return (
285
+ <div
286
+ key={item.value}
287
+ className={cn(
288
+ "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
289
+ )}
290
+ >
291
+ {itemConfig?.icon && !hideIcon ? (
292
+ <itemConfig.icon />
293
+ ) : (
294
+ <div
295
+ className="h-2 w-2 shrink-0 rounded-[2px]"
296
+ style={{
297
+ backgroundColor: item.color,
298
+ }}
299
+ />
300
+ )}
301
+ {itemConfig?.label}
302
+ </div>
303
+ );
304
+ })}
305
+ </div>
306
+ );
307
+ }
308
+
309
+ // Helper to extract item config from a payload.
310
+ function getPayloadConfigFromPayload(
311
+ config: ChartConfig,
312
+ payload: unknown,
313
+ key: string
314
+ ) {
315
+ if (typeof payload !== "object" || payload === null) {
316
+ return undefined;
317
+ }
318
+
319
+ const payloadPayload =
320
+ "payload" in payload &&
321
+ typeof payload.payload === "object" &&
322
+ payload.payload !== null
323
+ ? payload.payload
324
+ : undefined;
325
+
326
+ let configLabelKey: string = key;
327
+
328
+ if (
329
+ key in payload &&
330
+ typeof payload[key as keyof typeof payload] === "string"
331
+ ) {
332
+ configLabelKey = payload[key as keyof typeof payload] as string;
333
+ } else if (
334
+ payloadPayload &&
335
+ key in payloadPayload &&
336
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
337
+ ) {
338
+ configLabelKey = payloadPayload[
339
+ key as keyof typeof payloadPayload
340
+ ] as string;
341
+ }
342
+
343
+ return configLabelKey in config
344
+ ? config[configLabelKey]
345
+ : config[key as keyof typeof config];
346
+ }
347
+
348
+ export {
349
+ ChartContainer,
350
+ ChartTooltip,
351
+ ChartTooltipContent,
352
+ ChartLegend,
353
+ ChartLegendContent,
354
+ ChartStyle,
355
+ };
client/src/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
+ import { CheckIcon } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function Checkbox({
8
+ className,
9
+ ...props
10
+ }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
11
+ return (
12
+ <CheckboxPrimitive.Root
13
+ data-slot="checkbox"
14
+ className={cn(
15
+ "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
16
+ className
17
+ )}
18
+ {...props}
19
+ >
20
+ <CheckboxPrimitive.Indicator
21
+ data-slot="checkbox-indicator"
22
+ className="flex items-center justify-center text-current transition-none"
23
+ >
24
+ <CheckIcon className="size-3.5" />
25
+ </CheckboxPrimitive.Indicator>
26
+ </CheckboxPrimitive.Root>
27
+ );
28
+ }
29
+
30
+ export { Checkbox };
client/src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
2
+
3
+ function Collapsible({
4
+ ...props
5
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
6
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
7
+ }
8
+
9
+ function CollapsibleTrigger({
10
+ ...props
11
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
12
+ return (
13
+ <CollapsiblePrimitive.CollapsibleTrigger
14
+ data-slot="collapsible-trigger"
15
+ {...props}
16
+ />
17
+ );
18
+ }
19
+
20
+ function CollapsibleContent({
21
+ ...props
22
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
23
+ return (
24
+ <CollapsiblePrimitive.CollapsibleContent
25
+ data-slot="collapsible-content"
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
client/src/components/ui/command.tsx ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Command as CommandPrimitive } from "cmdk";
5
+ import { SearchIcon } from "lucide-react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from "@/components/ui/dialog";
15
+
16
+ function Command({
17
+ className,
18
+ ...props
19
+ }: React.ComponentProps<typeof CommandPrimitive>) {
20
+ return (
21
+ <CommandPrimitive
22
+ data-slot="command"
23
+ className={cn(
24
+ "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ );
30
+ }
31
+
32
+ function CommandDialog({
33
+ title = "Command Palette",
34
+ description = "Search for a command to run...",
35
+ children,
36
+ className,
37
+ showCloseButton = true,
38
+ ...props
39
+ }: React.ComponentProps<typeof Dialog> & {
40
+ title?: string;
41
+ description?: string;
42
+ className?: string;
43
+ showCloseButton?: boolean;
44
+ }) {
45
+ return (
46
+ <Dialog {...props}>
47
+ <DialogHeader className="sr-only">
48
+ <DialogTitle>{title}</DialogTitle>
49
+ <DialogDescription>{description}</DialogDescription>
50
+ </DialogHeader>
51
+ <DialogContent
52
+ className={cn("overflow-hidden p-0", className)}
53
+ showCloseButton={showCloseButton}
54
+ >
55
+ <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[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">
56
+ {children}
57
+ </Command>
58
+ </DialogContent>
59
+ </Dialog>
60
+ );
61
+ }
62
+
63
+ function CommandInput({
64
+ className,
65
+ ...props
66
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
67
+ return (
68
+ <div
69
+ data-slot="command-input-wrapper"
70
+ className="flex h-9 items-center gap-2 border-b px-3"
71
+ >
72
+ <SearchIcon className="size-4 shrink-0 opacity-50" />
73
+ <CommandPrimitive.Input
74
+ data-slot="command-input"
75
+ className={cn(
76
+ "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ </div>
82
+ );
83
+ }
84
+
85
+ function CommandList({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<typeof CommandPrimitive.List>) {
89
+ return (
90
+ <CommandPrimitive.List
91
+ data-slot="command-list"
92
+ className={cn(
93
+ "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
94
+ className
95
+ )}
96
+ {...props}
97
+ />
98
+ );
99
+ }
100
+
101
+ function CommandEmpty({
102
+ ...props
103
+ }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
104
+ return (
105
+ <CommandPrimitive.Empty
106
+ data-slot="command-empty"
107
+ className="py-6 text-center text-sm"
108
+ {...props}
109
+ />
110
+ );
111
+ }
112
+
113
+ function CommandGroup({
114
+ className,
115
+ ...props
116
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
117
+ return (
118
+ <CommandPrimitive.Group
119
+ data-slot="command-group"
120
+ className={cn(
121
+ "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ );
127
+ }
128
+
129
+ function CommandSeparator({
130
+ className,
131
+ ...props
132
+ }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
133
+ return (
134
+ <CommandPrimitive.Separator
135
+ data-slot="command-separator"
136
+ className={cn("bg-border -mx-1 h-px", className)}
137
+ {...props}
138
+ />
139
+ );
140
+ }
141
+
142
+ function CommandItem({
143
+ className,
144
+ ...props
145
+ }: React.ComponentProps<typeof CommandPrimitive.Item>) {
146
+ return (
147
+ <CommandPrimitive.Item
148
+ data-slot="command-item"
149
+ className={cn(
150
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
151
+ className
152
+ )}
153
+ {...props}
154
+ />
155
+ );
156
+ }
157
+
158
+ function CommandShortcut({
159
+ className,
160
+ ...props
161
+ }: React.ComponentProps<"span">) {
162
+ return (
163
+ <span
164
+ data-slot="command-shortcut"
165
+ className={cn(
166
+ "text-muted-foreground ml-auto text-xs tracking-widest",
167
+ className
168
+ )}
169
+ {...props}
170
+ />
171
+ );
172
+ }
173
+
174
+ export {
175
+ Command,
176
+ CommandDialog,
177
+ CommandInput,
178
+ CommandList,
179
+ CommandEmpty,
180
+ CommandGroup,
181
+ CommandItem,
182
+ CommandShortcut,
183
+ CommandSeparator,
184
+ };
client/src/components/ui/context-menu.tsx ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
3
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function ContextMenu({
8
+ ...props
9
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
10
+ return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
11
+ }
12
+
13
+ function ContextMenuTrigger({
14
+ ...props
15
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
16
+ return (
17
+ <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
18
+ );
19
+ }
20
+
21
+ function ContextMenuGroup({
22
+ ...props
23
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
24
+ return (
25
+ <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
26
+ );
27
+ }
28
+
29
+ function ContextMenuPortal({
30
+ ...props
31
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
32
+ return (
33
+ <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
34
+ );
35
+ }
36
+
37
+ function ContextMenuSub({
38
+ ...props
39
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
40
+ return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
41
+ }
42
+
43
+ function ContextMenuRadioGroup({
44
+ ...props
45
+ }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
46
+ return (
47
+ <ContextMenuPrimitive.RadioGroup
48
+ data-slot="context-menu-radio-group"
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ function ContextMenuSubTrigger({
55
+ className,
56
+ inset,
57
+ children,
58
+ ...props
59
+ }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
60
+ inset?: boolean;
61
+ }) {
62
+ return (
63
+ <ContextMenuPrimitive.SubTrigger
64
+ data-slot="context-menu-sub-trigger"
65
+ data-inset={inset}
66
+ className={cn(
67
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
68
+ className
69
+ )}
70
+ {...props}
71
+ >
72
+ {children}
73
+ <ChevronRightIcon className="ml-auto" />
74
+ </ContextMenuPrimitive.SubTrigger>
75
+ );
76
+ }
77
+
78
+ function ContextMenuSubContent({
79
+ className,
80
+ ...props
81
+ }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
82
+ return (
83
+ <ContextMenuPrimitive.SubContent
84
+ data-slot="context-menu-sub-content"
85
+ className={cn(
86
+ "bg-popover 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 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+
94
+ function ContextMenuContent({
95
+ className,
96
+ ...props
97
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
98
+ return (
99
+ <ContextMenuPrimitive.Portal>
100
+ <ContextMenuPrimitive.Content
101
+ data-slot="context-menu-content"
102
+ className={cn(
103
+ "bg-popover 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 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
104
+ className
105
+ )}
106
+ {...props}
107
+ />
108
+ </ContextMenuPrimitive.Portal>
109
+ );
110
+ }
111
+
112
+ function ContextMenuItem({
113
+ className,
114
+ inset,
115
+ variant = "default",
116
+ ...props
117
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
118
+ inset?: boolean;
119
+ variant?: "default" | "destructive";
120
+ }) {
121
+ return (
122
+ <ContextMenuPrimitive.Item
123
+ data-slot="context-menu-item"
124
+ data-inset={inset}
125
+ data-variant={variant}
126
+ className={cn(
127
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
128
+ className
129
+ )}
130
+ {...props}
131
+ />
132
+ );
133
+ }
134
+
135
+ function ContextMenuCheckboxItem({
136
+ className,
137
+ children,
138
+ checked,
139
+ ...props
140
+ }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
141
+ return (
142
+ <ContextMenuPrimitive.CheckboxItem
143
+ data-slot="context-menu-checkbox-item"
144
+ className={cn(
145
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
146
+ className
147
+ )}
148
+ checked={checked}
149
+ {...props}
150
+ >
151
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
152
+ <ContextMenuPrimitive.ItemIndicator>
153
+ <CheckIcon className="size-4" />
154
+ </ContextMenuPrimitive.ItemIndicator>
155
+ </span>
156
+ {children}
157
+ </ContextMenuPrimitive.CheckboxItem>
158
+ );
159
+ }
160
+
161
+ function ContextMenuRadioItem({
162
+ className,
163
+ children,
164
+ ...props
165
+ }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
166
+ return (
167
+ <ContextMenuPrimitive.RadioItem
168
+ data-slot="context-menu-radio-item"
169
+ className={cn(
170
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
171
+ className
172
+ )}
173
+ {...props}
174
+ >
175
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
176
+ <ContextMenuPrimitive.ItemIndicator>
177
+ <CircleIcon className="size-2 fill-current" />
178
+ </ContextMenuPrimitive.ItemIndicator>
179
+ </span>
180
+ {children}
181
+ </ContextMenuPrimitive.RadioItem>
182
+ );
183
+ }
184
+
185
+ function ContextMenuLabel({
186
+ className,
187
+ inset,
188
+ ...props
189
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
190
+ inset?: boolean;
191
+ }) {
192
+ return (
193
+ <ContextMenuPrimitive.Label
194
+ data-slot="context-menu-label"
195
+ data-inset={inset}
196
+ className={cn(
197
+ "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
198
+ className
199
+ )}
200
+ {...props}
201
+ />
202
+ );
203
+ }
204
+
205
+ function ContextMenuSeparator({
206
+ className,
207
+ ...props
208
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
209
+ return (
210
+ <ContextMenuPrimitive.Separator
211
+ data-slot="context-menu-separator"
212
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
213
+ {...props}
214
+ />
215
+ );
216
+ }
217
+
218
+ function ContextMenuShortcut({
219
+ className,
220
+ ...props
221
+ }: React.ComponentProps<"span">) {
222
+ return (
223
+ <span
224
+ data-slot="context-menu-shortcut"
225
+ className={cn(
226
+ "text-muted-foreground ml-auto text-xs tracking-widest",
227
+ className
228
+ )}
229
+ {...props}
230
+ />
231
+ );
232
+ }
233
+
234
+ export {
235
+ ContextMenu,
236
+ ContextMenuTrigger,
237
+ ContextMenuContent,
238
+ ContextMenuItem,
239
+ ContextMenuCheckboxItem,
240
+ ContextMenuRadioItem,
241
+ ContextMenuLabel,
242
+ ContextMenuSeparator,
243
+ ContextMenuShortcut,
244
+ ContextMenuGroup,
245
+ ContextMenuPortal,
246
+ ContextMenuSub,
247
+ ContextMenuSubContent,
248
+ ContextMenuSubTrigger,
249
+ ContextMenuRadioGroup,
250
+ };
client/src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils";
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import { XIcon } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ // Context to track composition state across dialog children
7
+ const DialogCompositionContext = React.createContext<{
8
+ isComposing: () => boolean;
9
+ setComposing: (composing: boolean) => void;
10
+ justEndedComposing: () => boolean;
11
+ markCompositionEnd: () => void;
12
+ }>({
13
+ isComposing: () => false,
14
+ setComposing: () => {},
15
+ justEndedComposing: () => false,
16
+ markCompositionEnd: () => {},
17
+ });
18
+
19
+ export const useDialogComposition = () =>
20
+ React.useContext(DialogCompositionContext);
21
+
22
+ function Dialog({
23
+ ...props
24
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
25
+ const composingRef = React.useRef(false);
26
+ const justEndedRef = React.useRef(false);
27
+ const endTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
28
+
29
+ const contextValue = React.useMemo(
30
+ () => ({
31
+ isComposing: () => composingRef.current,
32
+ setComposing: (composing: boolean) => {
33
+ composingRef.current = composing;
34
+ },
35
+ justEndedComposing: () => justEndedRef.current,
36
+ markCompositionEnd: () => {
37
+ justEndedRef.current = true;
38
+ if (endTimerRef.current) {
39
+ clearTimeout(endTimerRef.current);
40
+ }
41
+ endTimerRef.current = setTimeout(() => {
42
+ justEndedRef.current = false;
43
+ }, 150);
44
+ },
45
+ }),
46
+ []
47
+ );
48
+
49
+ return (
50
+ <DialogCompositionContext.Provider value={contextValue}>
51
+ <DialogPrimitive.Root data-slot="dialog" {...props} />
52
+ </DialogCompositionContext.Provider>
53
+ );
54
+ }
55
+
56
+ function DialogTrigger({
57
+ ...props
58
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
59
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
60
+ }
61
+
62
+ function DialogPortal({
63
+ ...props
64
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
65
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
66
+ }
67
+
68
+ function DialogClose({
69
+ ...props
70
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
71
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
72
+ }
73
+
74
+ function DialogOverlay({
75
+ className,
76
+ ...props
77
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
78
+ return (
79
+ <DialogPrimitive.Overlay
80
+ data-slot="dialog-overlay"
81
+ className={cn(
82
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
83
+ className
84
+ )}
85
+ {...props}
86
+ />
87
+ );
88
+ }
89
+
90
+ DialogOverlay.displayName = "DialogOverlay";
91
+
92
+ function DialogContent({
93
+ className,
94
+ children,
95
+ showCloseButton = true,
96
+ onEscapeKeyDown,
97
+ ...props
98
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
99
+ showCloseButton?: boolean;
100
+ }) {
101
+ const { isComposing } = useDialogComposition();
102
+
103
+ const handleEscapeKeyDown = React.useCallback(
104
+ (e: KeyboardEvent) => {
105
+ // Check both the native isComposing property and our context state
106
+ // This handles Safari's timing issues with composition events
107
+ const isCurrentlyComposing = (e as any).isComposing || isComposing();
108
+
109
+ // If IME is composing, prevent dialog from closing
110
+ if (isCurrentlyComposing) {
111
+ e.preventDefault();
112
+ return;
113
+ }
114
+
115
+ // Call user's onEscapeKeyDown if provided
116
+ onEscapeKeyDown?.(e);
117
+ },
118
+ [isComposing, onEscapeKeyDown]
119
+ );
120
+
121
+ return (
122
+ <DialogPortal data-slot="dialog-portal">
123
+ <DialogOverlay />
124
+ <DialogPrimitive.Content
125
+ data-slot="dialog-content"
126
+ className={cn(
127
+ "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
128
+ className
129
+ )}
130
+ onEscapeKeyDown={handleEscapeKeyDown}
131
+ {...props}
132
+ >
133
+ {children}
134
+ {showCloseButton && (
135
+ <DialogPrimitive.Close
136
+ data-slot="dialog-close"
137
+ className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
138
+ >
139
+ <XIcon />
140
+ <span className="sr-only">Close</span>
141
+ </DialogPrimitive.Close>
142
+ )}
143
+ </DialogPrimitive.Content>
144
+ </DialogPortal>
145
+ );
146
+ }
147
+
148
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
149
+ return (
150
+ <div
151
+ data-slot="dialog-header"
152
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
153
+ {...props}
154
+ />
155
+ );
156
+ }
157
+
158
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
159
+ return (
160
+ <div
161
+ data-slot="dialog-footer"
162
+ className={cn(
163
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
164
+ className
165
+ )}
166
+ {...props}
167
+ />
168
+ );
169
+ }
170
+
171
+ function DialogTitle({
172
+ className,
173
+ ...props
174
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
175
+ return (
176
+ <DialogPrimitive.Title
177
+ data-slot="dialog-title"
178
+ className={cn("text-lg leading-none font-semibold", className)}
179
+ {...props}
180
+ />
181
+ );
182
+ }
183
+
184
+ function DialogDescription({
185
+ className,
186
+ ...props
187
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
188
+ return (
189
+ <DialogPrimitive.Description
190
+ data-slot="dialog-description"
191
+ className={cn("text-muted-foreground text-sm", className)}
192
+ {...props}
193
+ />
194
+ );
195
+ }
196
+
197
+ export {
198
+ Dialog,
199
+ DialogClose,
200
+ DialogContent,
201
+ DialogDescription,
202
+ DialogFooter,
203
+ DialogHeader,
204
+ DialogOverlay,
205
+ DialogPortal,
206
+ DialogTitle,
207
+ DialogTrigger
208
+ };
209
+
client/src/components/ui/drawer.tsx ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Drawer as DrawerPrimitive } from "vaul";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function Drawer({
7
+ ...props
8
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
9
+ return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
10
+ }
11
+
12
+ function DrawerTrigger({
13
+ ...props
14
+ }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
15
+ return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
16
+ }
17
+
18
+ function DrawerPortal({
19
+ ...props
20
+ }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
21
+ return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
22
+ }
23
+
24
+ function DrawerClose({
25
+ ...props
26
+ }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
27
+ return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
28
+ }
29
+
30
+ function DrawerOverlay({
31
+ className,
32
+ ...props
33
+ }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
34
+ return (
35
+ <DrawerPrimitive.Overlay
36
+ data-slot="drawer-overlay"
37
+ className={cn(
38
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
39
+ className
40
+ )}
41
+ {...props}
42
+ />
43
+ );
44
+ }
45
+
46
+ function DrawerContent({
47
+ className,
48
+ children,
49
+ ...props
50
+ }: React.ComponentProps<typeof DrawerPrimitive.Content>) {
51
+ return (
52
+ <DrawerPortal data-slot="drawer-portal">
53
+ <DrawerOverlay />
54
+ <DrawerPrimitive.Content
55
+ data-slot="drawer-content"
56
+ className={cn(
57
+ "group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
58
+ "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
59
+ "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
60
+ "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
61
+ "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
62
+ className
63
+ )}
64
+ {...props}
65
+ >
66
+ <div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
67
+ {children}
68
+ </DrawerPrimitive.Content>
69
+ </DrawerPortal>
70
+ );
71
+ }
72
+
73
+ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
74
+ return (
75
+ <div
76
+ data-slot="drawer-header"
77
+ className={cn(
78
+ "flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
79
+ className
80
+ )}
81
+ {...props}
82
+ />
83
+ );
84
+ }
85
+
86
+ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
87
+ return (
88
+ <div
89
+ data-slot="drawer-footer"
90
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
91
+ {...props}
92
+ />
93
+ );
94
+ }
95
+
96
+ function DrawerTitle({
97
+ className,
98
+ ...props
99
+ }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
100
+ return (
101
+ <DrawerPrimitive.Title
102
+ data-slot="drawer-title"
103
+ className={cn("text-foreground font-semibold", className)}
104
+ {...props}
105
+ />
106
+ );
107
+ }
108
+
109
+ function DrawerDescription({
110
+ className,
111
+ ...props
112
+ }: React.ComponentProps<typeof DrawerPrimitive.Description>) {
113
+ return (
114
+ <DrawerPrimitive.Description
115
+ data-slot="drawer-description"
116
+ className={cn("text-muted-foreground text-sm", className)}
117
+ {...props}
118
+ />
119
+ );
120
+ }
121
+
122
+ export {
123
+ Drawer,
124
+ DrawerPortal,
125
+ DrawerOverlay,
126
+ DrawerTrigger,
127
+ DrawerClose,
128
+ DrawerContent,
129
+ DrawerHeader,
130
+ DrawerFooter,
131
+ DrawerTitle,
132
+ DrawerDescription,
133
+ };
client/src/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
3
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function DropdownMenu({
8
+ ...props
9
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
10
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
11
+ }
12
+
13
+ function DropdownMenuPortal({
14
+ ...props
15
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
16
+ return (
17
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
18
+ );
19
+ }
20
+
21
+ function DropdownMenuTrigger({
22
+ ...props
23
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
24
+ return (
25
+ <DropdownMenuPrimitive.Trigger
26
+ data-slot="dropdown-menu-trigger"
27
+ {...props}
28
+ />
29
+ );
30
+ }
31
+
32
+ function DropdownMenuContent({
33
+ className,
34
+ sideOffset = 4,
35
+ ...props
36
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
37
+ return (
38
+ <DropdownMenuPrimitive.Portal>
39
+ <DropdownMenuPrimitive.Content
40
+ data-slot="dropdown-menu-content"
41
+ sideOffset={sideOffset}
42
+ className={cn(
43
+ "bg-popover 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
44
+ className
45
+ )}
46
+ {...props}
47
+ />
48
+ </DropdownMenuPrimitive.Portal>
49
+ );
50
+ }
51
+
52
+ function DropdownMenuGroup({
53
+ ...props
54
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
55
+ return (
56
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
57
+ );
58
+ }
59
+
60
+ function DropdownMenuItem({
61
+ className,
62
+ inset,
63
+ variant = "default",
64
+ ...props
65
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
66
+ inset?: boolean;
67
+ variant?: "default" | "destructive";
68
+ }) {
69
+ return (
70
+ <DropdownMenuPrimitive.Item
71
+ data-slot="dropdown-menu-item"
72
+ data-inset={inset}
73
+ data-variant={variant}
74
+ className={cn(
75
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
76
+ className
77
+ )}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ function DropdownMenuCheckboxItem({
84
+ className,
85
+ children,
86
+ checked,
87
+ ...props
88
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
89
+ return (
90
+ <DropdownMenuPrimitive.CheckboxItem
91
+ data-slot="dropdown-menu-checkbox-item"
92
+ className={cn(
93
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
94
+ className
95
+ )}
96
+ checked={checked}
97
+ {...props}
98
+ >
99
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
100
+ <DropdownMenuPrimitive.ItemIndicator>
101
+ <CheckIcon className="size-4" />
102
+ </DropdownMenuPrimitive.ItemIndicator>
103
+ </span>
104
+ {children}
105
+ </DropdownMenuPrimitive.CheckboxItem>
106
+ );
107
+ }
108
+
109
+ function DropdownMenuRadioGroup({
110
+ ...props
111
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
112
+ return (
113
+ <DropdownMenuPrimitive.RadioGroup
114
+ data-slot="dropdown-menu-radio-group"
115
+ {...props}
116
+ />
117
+ );
118
+ }
119
+
120
+ function DropdownMenuRadioItem({
121
+ className,
122
+ children,
123
+ ...props
124
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
125
+ return (
126
+ <DropdownMenuPrimitive.RadioItem
127
+ data-slot="dropdown-menu-radio-item"
128
+ className={cn(
129
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
130
+ className
131
+ )}
132
+ {...props}
133
+ >
134
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
135
+ <DropdownMenuPrimitive.ItemIndicator>
136
+ <CircleIcon className="size-2 fill-current" />
137
+ </DropdownMenuPrimitive.ItemIndicator>
138
+ </span>
139
+ {children}
140
+ </DropdownMenuPrimitive.RadioItem>
141
+ );
142
+ }
143
+
144
+ function DropdownMenuLabel({
145
+ className,
146
+ inset,
147
+ ...props
148
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
149
+ inset?: boolean;
150
+ }) {
151
+ return (
152
+ <DropdownMenuPrimitive.Label
153
+ data-slot="dropdown-menu-label"
154
+ data-inset={inset}
155
+ className={cn(
156
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
157
+ className
158
+ )}
159
+ {...props}
160
+ />
161
+ );
162
+ }
163
+
164
+ function DropdownMenuSeparator({
165
+ className,
166
+ ...props
167
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
168
+ return (
169
+ <DropdownMenuPrimitive.Separator
170
+ data-slot="dropdown-menu-separator"
171
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
172
+ {...props}
173
+ />
174
+ );
175
+ }
176
+
177
+ function DropdownMenuShortcut({
178
+ className,
179
+ ...props
180
+ }: React.ComponentProps<"span">) {
181
+ return (
182
+ <span
183
+ data-slot="dropdown-menu-shortcut"
184
+ className={cn(
185
+ "text-muted-foreground ml-auto text-xs tracking-widest",
186
+ className
187
+ )}
188
+ {...props}
189
+ />
190
+ );
191
+ }
192
+
193
+ function DropdownMenuSub({
194
+ ...props
195
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
196
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
197
+ }
198
+
199
+ function DropdownMenuSubTrigger({
200
+ className,
201
+ inset,
202
+ children,
203
+ ...props
204
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
205
+ inset?: boolean;
206
+ }) {
207
+ return (
208
+ <DropdownMenuPrimitive.SubTrigger
209
+ data-slot="dropdown-menu-sub-trigger"
210
+ data-inset={inset}
211
+ className={cn(
212
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
213
+ className
214
+ )}
215
+ {...props}
216
+ >
217
+ {children}
218
+ <ChevronRightIcon className="ml-auto size-4" />
219
+ </DropdownMenuPrimitive.SubTrigger>
220
+ );
221
+ }
222
+
223
+ function DropdownMenuSubContent({
224
+ className,
225
+ ...props
226
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
227
+ return (
228
+ <DropdownMenuPrimitive.SubContent
229
+ data-slot="dropdown-menu-sub-content"
230
+ className={cn(
231
+ "bg-popover 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
232
+ className
233
+ )}
234
+ {...props}
235
+ />
236
+ );
237
+ }
238
+
239
+ export {
240
+ DropdownMenu,
241
+ DropdownMenuPortal,
242
+ DropdownMenuTrigger,
243
+ DropdownMenuContent,
244
+ DropdownMenuGroup,
245
+ DropdownMenuLabel,
246
+ DropdownMenuItem,
247
+ DropdownMenuCheckboxItem,
248
+ DropdownMenuRadioGroup,
249
+ DropdownMenuRadioItem,
250
+ DropdownMenuSeparator,
251
+ DropdownMenuShortcut,
252
+ DropdownMenuSub,
253
+ DropdownMenuSubTrigger,
254
+ DropdownMenuSubContent,
255
+ };
client/src/components/ui/empty.tsx ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Empty({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="empty"
9
+ className={cn(
10
+ "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="empty-header"
22
+ className={cn(
23
+ "flex max-w-sm flex-col items-center gap-2 text-center",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ const emptyMediaVariants = cva(
32
+ "flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
33
+ {
34
+ variants: {
35
+ variant: {
36
+ default: "bg-transparent",
37
+ icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
38
+ },
39
+ },
40
+ defaultVariants: {
41
+ variant: "default",
42
+ },
43
+ }
44
+ );
45
+
46
+ function EmptyMedia({
47
+ className,
48
+ variant = "default",
49
+ ...props
50
+ }: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
51
+ return (
52
+ <div
53
+ data-slot="empty-icon"
54
+ data-variant={variant}
55
+ className={cn(emptyMediaVariants({ variant, className }))}
56
+ {...props}
57
+ />
58
+ );
59
+ }
60
+
61
+ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
62
+ return (
63
+ <div
64
+ data-slot="empty-title"
65
+ className={cn("text-lg font-medium tracking-tight", className)}
66
+ {...props}
67
+ />
68
+ );
69
+ }
70
+
71
+ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
72
+ return (
73
+ <div
74
+ data-slot="empty-description"
75
+ className={cn(
76
+ "text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ );
82
+ }
83
+
84
+ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
85
+ return (
86
+ <div
87
+ data-slot="empty-content"
88
+ className={cn(
89
+ "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
90
+ className
91
+ )}
92
+ {...props}
93
+ />
94
+ );
95
+ }
96
+
97
+ export {
98
+ Empty,
99
+ EmptyHeader,
100
+ EmptyTitle,
101
+ EmptyDescription,
102
+ EmptyContent,
103
+ EmptyMedia,
104
+ };
client/src/components/ui/field.tsx ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo } from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+ import { Label } from "@/components/ui/label";
6
+ import { Separator } from "@/components/ui/separator";
7
+
8
+ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
9
+ return (
10
+ <fieldset
11
+ data-slot="field-set"
12
+ className={cn(
13
+ "flex flex-col gap-6",
14
+ "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function FieldLegend({
23
+ className,
24
+ variant = "legend",
25
+ ...props
26
+ }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
27
+ return (
28
+ <legend
29
+ data-slot="field-legend"
30
+ data-variant={variant}
31
+ className={cn(
32
+ "mb-3 font-medium",
33
+ "data-[variant=legend]:text-base",
34
+ "data-[variant=label]:text-sm",
35
+ className
36
+ )}
37
+ {...props}
38
+ />
39
+ );
40
+ }
41
+
42
+ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
43
+ return (
44
+ <div
45
+ data-slot="field-group"
46
+ className={cn(
47
+ "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ const fieldVariants = cva(
56
+ "group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
57
+ {
58
+ variants: {
59
+ orientation: {
60
+ vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
61
+ horizontal: [
62
+ "flex-row items-center",
63
+ "[&>[data-slot=field-label]]:flex-auto",
64
+ "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
65
+ ],
66
+ responsive: [
67
+ "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
68
+ "@md/field-group:[&>[data-slot=field-label]]:flex-auto",
69
+ "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
70
+ ],
71
+ },
72
+ },
73
+ defaultVariants: {
74
+ orientation: "vertical",
75
+ },
76
+ }
77
+ );
78
+
79
+ function Field({
80
+ className,
81
+ orientation = "vertical",
82
+ ...props
83
+ }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
84
+ return (
85
+ <div
86
+ role="group"
87
+ data-slot="field"
88
+ data-orientation={orientation}
89
+ className={cn(fieldVariants({ orientation }), className)}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
96
+ return (
97
+ <div
98
+ data-slot="field-content"
99
+ className={cn(
100
+ "group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
101
+ className
102
+ )}
103
+ {...props}
104
+ />
105
+ );
106
+ }
107
+
108
+ function FieldLabel({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof Label>) {
112
+ return (
113
+ <Label
114
+ data-slot="field-label"
115
+ className={cn(
116
+ "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
117
+ "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
118
+ "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
119
+ className
120
+ )}
121
+ {...props}
122
+ />
123
+ );
124
+ }
125
+
126
+ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
127
+ return (
128
+ <div
129
+ data-slot="field-label"
130
+ className={cn(
131
+ "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
132
+ className
133
+ )}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
140
+ return (
141
+ <p
142
+ data-slot="field-description"
143
+ className={cn(
144
+ "text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
145
+ "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
146
+ "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
147
+ className
148
+ )}
149
+ {...props}
150
+ />
151
+ );
152
+ }
153
+
154
+ function FieldSeparator({
155
+ children,
156
+ className,
157
+ ...props
158
+ }: React.ComponentProps<"div"> & {
159
+ children?: React.ReactNode;
160
+ }) {
161
+ return (
162
+ <div
163
+ data-slot="field-separator"
164
+ data-content={!!children}
165
+ className={cn(
166
+ "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
167
+ className
168
+ )}
169
+ {...props}
170
+ >
171
+ <Separator className="absolute inset-0 top-1/2" />
172
+ {children && (
173
+ <span
174
+ className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
175
+ data-slot="field-separator-content"
176
+ >
177
+ {children}
178
+ </span>
179
+ )}
180
+ </div>
181
+ );
182
+ }
183
+
184
+ function FieldError({
185
+ className,
186
+ children,
187
+ errors,
188
+ ...props
189
+ }: React.ComponentProps<"div"> & {
190
+ errors?: Array<{ message?: string } | undefined>;
191
+ }) {
192
+ const content = useMemo(() => {
193
+ if (children) {
194
+ return children;
195
+ }
196
+
197
+ if (!errors) {
198
+ return null;
199
+ }
200
+
201
+ if (errors?.length === 1 && errors[0]?.message) {
202
+ return errors[0].message;
203
+ }
204
+
205
+ return (
206
+ <ul className="ml-4 flex list-disc flex-col gap-1">
207
+ {errors.map(
208
+ (error, index) =>
209
+ error?.message && <li key={index}>{error.message}</li>
210
+ )}
211
+ </ul>
212
+ );
213
+ }, [children, errors]);
214
+
215
+ if (!content) {
216
+ return null;
217
+ }
218
+
219
+ return (
220
+ <div
221
+ role="alert"
222
+ data-slot="field-error"
223
+ className={cn("text-destructive text-sm font-normal", className)}
224
+ {...props}
225
+ >
226
+ {content}
227
+ </div>
228
+ );
229
+ }
230
+
231
+ export {
232
+ Field,
233
+ FieldLabel,
234
+ FieldDescription,
235
+ FieldError,
236
+ FieldGroup,
237
+ FieldLegend,
238
+ FieldSeparator,
239
+ FieldSet,
240
+ FieldContent,
241
+ FieldTitle,
242
+ };
client/src/components/ui/form.tsx ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ useFormState,
11
+ type ControllerProps,
12
+ type FieldPath,
13
+ type FieldValues,
14
+ } from "react-hook-form";
15
+
16
+ import { cn } from "@/lib/utils";
17
+ import { Label } from "@/components/ui/label";
18
+
19
+ const Form = FormProvider;
20
+
21
+ type FormFieldContextValue<
22
+ TFieldValues extends FieldValues = FieldValues,
23
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
24
+ > = {
25
+ name: TName;
26
+ };
27
+
28
+ const FormFieldContext = React.createContext<FormFieldContextValue>(
29
+ {} as FormFieldContextValue
30
+ );
31
+
32
+ const FormField = <
33
+ TFieldValues extends FieldValues = FieldValues,
34
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
35
+ >({
36
+ ...props
37
+ }: ControllerProps<TFieldValues, TName>) => {
38
+ return (
39
+ <FormFieldContext.Provider value={{ name: props.name }}>
40
+ <Controller {...props} />
41
+ </FormFieldContext.Provider>
42
+ );
43
+ };
44
+
45
+ const useFormField = () => {
46
+ const fieldContext = React.useContext(FormFieldContext);
47
+ const itemContext = React.useContext(FormItemContext);
48
+ const { getFieldState } = useFormContext();
49
+ const formState = useFormState({ name: fieldContext.name });
50
+ const fieldState = getFieldState(fieldContext.name, formState);
51
+
52
+ if (!fieldContext) {
53
+ throw new Error("useFormField should be used within <FormField>");
54
+ }
55
+
56
+ const { id } = itemContext;
57
+
58
+ return {
59
+ id,
60
+ name: fieldContext.name,
61
+ formItemId: `${id}-form-item`,
62
+ formDescriptionId: `${id}-form-item-description`,
63
+ formMessageId: `${id}-form-item-message`,
64
+ ...fieldState,
65
+ };
66
+ };
67
+
68
+ type FormItemContextValue = {
69
+ id: string;
70
+ };
71
+
72
+ const FormItemContext = React.createContext<FormItemContextValue>(
73
+ {} as FormItemContextValue
74
+ );
75
+
76
+ function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77
+ const id = React.useId();
78
+
79
+ return (
80
+ <FormItemContext.Provider value={{ id }}>
81
+ <div
82
+ data-slot="form-item"
83
+ className={cn("grid gap-2", className)}
84
+ {...props}
85
+ />
86
+ </FormItemContext.Provider>
87
+ );
88
+ }
89
+
90
+ function FormLabel({
91
+ className,
92
+ ...props
93
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
94
+ const { error, formItemId } = useFormField();
95
+
96
+ return (
97
+ <Label
98
+ data-slot="form-label"
99
+ data-error={!!error}
100
+ className={cn("data-[error=true]:text-destructive", className)}
101
+ htmlFor={formItemId}
102
+ {...props}
103
+ />
104
+ );
105
+ }
106
+
107
+ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
108
+ const { error, formItemId, formDescriptionId, formMessageId } =
109
+ useFormField();
110
+
111
+ return (
112
+ <Slot
113
+ data-slot="form-control"
114
+ id={formItemId}
115
+ aria-describedby={
116
+ !error
117
+ ? `${formDescriptionId}`
118
+ : `${formDescriptionId} ${formMessageId}`
119
+ }
120
+ aria-invalid={!!error}
121
+ {...props}
122
+ />
123
+ );
124
+ }
125
+
126
+ function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
127
+ const { formDescriptionId } = useFormField();
128
+
129
+ return (
130
+ <p
131
+ data-slot="form-description"
132
+ id={formDescriptionId}
133
+ className={cn("text-muted-foreground text-sm", className)}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
140
+ const { error, formMessageId } = useFormField();
141
+ const body = error ? String(error?.message ?? "") : props.children;
142
+
143
+ if (!body) {
144
+ return null;
145
+ }
146
+
147
+ return (
148
+ <p
149
+ data-slot="form-message"
150
+ id={formMessageId}
151
+ className={cn("text-destructive text-sm", className)}
152
+ {...props}
153
+ >
154
+ {body}
155
+ </p>
156
+ );
157
+ }
158
+
159
+ export {
160
+ useFormField,
161
+ Form,
162
+ FormItem,
163
+ FormLabel,
164
+ FormControl,
165
+ FormDescription,
166
+ FormMessage,
167
+ FormField,
168
+ };
client/src/components/ui/hover-card.tsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function HoverCard({
7
+ ...props
8
+ }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
9
+ return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
10
+ }
11
+
12
+ function HoverCardTrigger({
13
+ ...props
14
+ }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
15
+ return (
16
+ <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
17
+ );
18
+ }
19
+
20
+ function HoverCardContent({
21
+ className,
22
+ align = "center",
23
+ sideOffset = 4,
24
+ ...props
25
+ }: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
26
+ return (
27
+ <HoverCardPrimitive.Portal data-slot="hover-card-portal">
28
+ <HoverCardPrimitive.Content
29
+ data-slot="hover-card-content"
30
+ align={align}
31
+ sideOffset={sideOffset}
32
+ className={cn(
33
+ "bg-popover 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 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ </HoverCardPrimitive.Portal>
39
+ );
40
+ }
41
+
42
+ export { HoverCard, HoverCardTrigger, HoverCardContent };
client/src/components/ui/input-group.tsx ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Textarea } from "@/components/ui/textarea";
8
+
9
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
10
+ return (
11
+ <div
12
+ data-slot="input-group"
13
+ role="group"
14
+ className={cn(
15
+ "group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
16
+ "h-9 min-w-0 has-[>textarea]:h-auto",
17
+
18
+ // Variants based on alignment.
19
+ "has-[>[data-align=inline-start]]:[&>input]:pl-2",
20
+ "has-[>[data-align=inline-end]]:[&>input]:pr-2",
21
+ "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
22
+ "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
23
+
24
+ // Focus state.
25
+ "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
26
+
27
+ // Error state.
28
+ "has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
29
+
30
+ className
31
+ )}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ const inputGroupAddonVariants = cva(
38
+ "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
39
+ {
40
+ variants: {
41
+ align: {
42
+ "inline-start":
43
+ "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
44
+ "inline-end":
45
+ "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
46
+ "block-start":
47
+ "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
48
+ "block-end":
49
+ "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
50
+ },
51
+ },
52
+ defaultVariants: {
53
+ align: "inline-start",
54
+ },
55
+ }
56
+ );
57
+
58
+ function InputGroupAddon({
59
+ className,
60
+ align = "inline-start",
61
+ ...props
62
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
63
+ return (
64
+ <div
65
+ role="group"
66
+ data-slot="input-group-addon"
67
+ data-align={align}
68
+ className={cn(inputGroupAddonVariants({ align }), className)}
69
+ onClick={e => {
70
+ if ((e.target as HTMLElement).closest("button")) {
71
+ return;
72
+ }
73
+ e.currentTarget.parentElement?.querySelector("input")?.focus();
74
+ }}
75
+ {...props}
76
+ />
77
+ );
78
+ }
79
+
80
+ const inputGroupButtonVariants = cva(
81
+ "text-sm shadow-none flex gap-2 items-center",
82
+ {
83
+ variants: {
84
+ size: {
85
+ xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
86
+ sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
87
+ "icon-xs":
88
+ "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
89
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0",
90
+ },
91
+ },
92
+ defaultVariants: {
93
+ size: "xs",
94
+ },
95
+ }
96
+ );
97
+
98
+ function InputGroupButton({
99
+ className,
100
+ type = "button",
101
+ variant = "ghost",
102
+ size = "xs",
103
+ ...props
104
+ }: Omit<React.ComponentProps<typeof Button>, "size"> &
105
+ VariantProps<typeof inputGroupButtonVariants>) {
106
+ return (
107
+ <Button
108
+ type={type}
109
+ data-size={size}
110
+ variant={variant}
111
+ className={cn(inputGroupButtonVariants({ size }), className)}
112
+ {...props}
113
+ />
114
+ );
115
+ }
116
+
117
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
118
+ return (
119
+ <span
120
+ className={cn(
121
+ "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ );
127
+ }
128
+
129
+ function InputGroupInput({
130
+ className,
131
+ ...props
132
+ }: React.ComponentProps<"input">) {
133
+ return (
134
+ <Input
135
+ data-slot="input-group-control"
136
+ className={cn(
137
+ "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
138
+ className
139
+ )}
140
+ {...props}
141
+ />
142
+ );
143
+ }
144
+
145
+ function InputGroupTextarea({
146
+ className,
147
+ ...props
148
+ }: React.ComponentProps<"textarea">) {
149
+ return (
150
+ <Textarea
151
+ data-slot="input-group-control"
152
+ className={cn(
153
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
154
+ className
155
+ )}
156
+ {...props}
157
+ />
158
+ );
159
+ }
160
+
161
+ export {
162
+ InputGroup,
163
+ InputGroupAddon,
164
+ InputGroupButton,
165
+ InputGroupText,
166
+ InputGroupInput,
167
+ InputGroupTextarea,
168
+ };
client/src/components/ui/input-otp.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { OTPInput, OTPInputContext } from "input-otp";
3
+ import { MinusIcon } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function InputOTP({
8
+ className,
9
+ containerClassName,
10
+ ...props
11
+ }: React.ComponentProps<typeof OTPInput> & {
12
+ containerClassName?: string;
13
+ }) {
14
+ return (
15
+ <OTPInput
16
+ data-slot="input-otp"
17
+ containerClassName={cn(
18
+ "flex items-center gap-2 has-disabled:opacity-50",
19
+ containerClassName
20
+ )}
21
+ className={cn("disabled:cursor-not-allowed", className)}
22
+ {...props}
23
+ />
24
+ );
25
+ }
26
+
27
+ function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
28
+ return (
29
+ <div
30
+ data-slot="input-otp-group"
31
+ className={cn("flex items-center", className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function InputOTPSlot({
38
+ index,
39
+ className,
40
+ ...props
41
+ }: React.ComponentProps<"div"> & {
42
+ index: number;
43
+ }) {
44
+ const inputOTPContext = React.useContext(OTPInputContext);
45
+ const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
46
+
47
+ return (
48
+ <div
49
+ data-slot="input-otp-slot"
50
+ data-active={isActive}
51
+ className={cn(
52
+ "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
53
+ className
54
+ )}
55
+ {...props}
56
+ >
57
+ {char}
58
+ {hasFakeCaret && (
59
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
60
+ <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
61
+ </div>
62
+ )}
63
+ </div>
64
+ );
65
+ }
66
+
67
+ function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
68
+ return (
69
+ <div data-slot="input-otp-separator" role="separator" {...props}>
70
+ <MinusIcon />
71
+ </div>
72
+ );
73
+ }
74
+
75
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
client/src/components/ui/input.tsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useDialogComposition } from "@/components/ui/dialog";
2
+ import { useComposition } from "@/hooks/useComposition";
3
+ import { cn } from "@/lib/utils";
4
+ import * as React from "react";
5
+
6
+ function Input({
7
+ className,
8
+ type,
9
+ onKeyDown,
10
+ onCompositionStart,
11
+ onCompositionEnd,
12
+ ...props
13
+ }: React.ComponentProps<"input">) {
14
+ // Get dialog composition context if available (will be no-op if not inside Dialog)
15
+ const dialogComposition = useDialogComposition();
16
+
17
+ // Add composition event handlers to support input method editor (IME) for CJK languages.
18
+ const {
19
+ onCompositionStart: handleCompositionStart,
20
+ onCompositionEnd: handleCompositionEnd,
21
+ onKeyDown: handleKeyDown,
22
+ } = useComposition<HTMLInputElement>({
23
+ onKeyDown: (e) => {
24
+ // Check if this is an Enter key that should be blocked
25
+ const isComposing = (e.nativeEvent as any).isComposing || dialogComposition.justEndedComposing();
26
+
27
+ // If Enter key is pressed while composing or just after composition ended,
28
+ // don't call the user's onKeyDown (this blocks the business logic)
29
+ if (e.key === "Enter" && isComposing) {
30
+ return;
31
+ }
32
+
33
+ // Otherwise, call the user's onKeyDown
34
+ onKeyDown?.(e);
35
+ },
36
+ onCompositionStart: e => {
37
+ dialogComposition.setComposing(true);
38
+ onCompositionStart?.(e);
39
+ },
40
+ onCompositionEnd: e => {
41
+ // Mark that composition just ended - this helps handle the Enter key that confirms input
42
+ dialogComposition.markCompositionEnd();
43
+ // Delay setting composing to false to handle Safari's event order
44
+ // In Safari, compositionEnd fires before the ESC keydown event
45
+ setTimeout(() => {
46
+ dialogComposition.setComposing(false);
47
+ }, 100);
48
+ onCompositionEnd?.(e);
49
+ },
50
+ });
51
+
52
+ return (
53
+ <input
54
+ type={type}
55
+ data-slot="input"
56
+ className={cn(
57
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
58
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
59
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
60
+ className
61
+ )}
62
+ onCompositionStart={handleCompositionStart}
63
+ onCompositionEnd={handleCompositionEnd}
64
+ onKeyDown={handleKeyDown}
65
+ {...props}
66
+ />
67
+ );
68
+ }
69
+
70
+ export { Input };
client/src/components/ui/item.tsx ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import { Separator } from "@/components/ui/separator";
7
+
8
+ function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
9
+ return (
10
+ <div
11
+ role="list"
12
+ data-slot="item-group"
13
+ className={cn("group/item-group flex flex-col", className)}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ function ItemSeparator({
20
+ className,
21
+ ...props
22
+ }: React.ComponentProps<typeof Separator>) {
23
+ return (
24
+ <Separator
25
+ data-slot="item-separator"
26
+ orientation="horizontal"
27
+ className={cn("my-0", className)}
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ const itemVariants = cva(
34
+ "group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
35
+ {
36
+ variants: {
37
+ variant: {
38
+ default: "bg-transparent",
39
+ outline: "border-border",
40
+ muted: "bg-muted/50",
41
+ },
42
+ size: {
43
+ default: "p-4 gap-4 ",
44
+ sm: "py-3 px-4 gap-2.5",
45
+ },
46
+ },
47
+ defaultVariants: {
48
+ variant: "default",
49
+ size: "default",
50
+ },
51
+ }
52
+ );
53
+
54
+ function Item({
55
+ className,
56
+ variant = "default",
57
+ size = "default",
58
+ asChild = false,
59
+ ...props
60
+ }: React.ComponentProps<"div"> &
61
+ VariantProps<typeof itemVariants> & { asChild?: boolean }) {
62
+ const Comp = asChild ? Slot : "div";
63
+ return (
64
+ <Comp
65
+ data-slot="item"
66
+ data-variant={variant}
67
+ data-size={size}
68
+ className={cn(itemVariants({ variant, size, className }))}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ const itemMediaVariants = cva(
75
+ "flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
76
+ {
77
+ variants: {
78
+ variant: {
79
+ default: "bg-transparent",
80
+ icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
81
+ image:
82
+ "size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
83
+ },
84
+ },
85
+ defaultVariants: {
86
+ variant: "default",
87
+ },
88
+ }
89
+ );
90
+
91
+ function ItemMedia({
92
+ className,
93
+ variant = "default",
94
+ ...props
95
+ }: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
96
+ return (
97
+ <div
98
+ data-slot="item-media"
99
+ data-variant={variant}
100
+ className={cn(itemMediaVariants({ variant, className }))}
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
107
+ return (
108
+ <div
109
+ data-slot="item-content"
110
+ className={cn(
111
+ "flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
112
+ className
113
+ )}
114
+ {...props}
115
+ />
116
+ );
117
+ }
118
+
119
+ function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
120
+ return (
121
+ <div
122
+ data-slot="item-title"
123
+ className={cn(
124
+ "flex w-fit items-center gap-2 text-sm leading-snug font-medium",
125
+ className
126
+ )}
127
+ {...props}
128
+ />
129
+ );
130
+ }
131
+
132
+ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
133
+ return (
134
+ <p
135
+ data-slot="item-description"
136
+ className={cn(
137
+ "text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
138
+ "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
139
+ className
140
+ )}
141
+ {...props}
142
+ />
143
+ );
144
+ }
145
+
146
+ function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
147
+ return (
148
+ <div
149
+ data-slot="item-actions"
150
+ className={cn("flex items-center gap-2", className)}
151
+ {...props}
152
+ />
153
+ );
154
+ }
155
+
156
+ function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
157
+ return (
158
+ <div
159
+ data-slot="item-header"
160
+ className={cn(
161
+ "flex basis-full items-center justify-between gap-2",
162
+ className
163
+ )}
164
+ {...props}
165
+ />
166
+ );
167
+ }
168
+
169
+ function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
170
+ return (
171
+ <div
172
+ data-slot="item-footer"
173
+ className={cn(
174
+ "flex basis-full items-center justify-between gap-2",
175
+ className
176
+ )}
177
+ {...props}
178
+ />
179
+ );
180
+ }
181
+
182
+ export {
183
+ Item,
184
+ ItemMedia,
185
+ ItemContent,
186
+ ItemActions,
187
+ ItemGroup,
188
+ ItemSeparator,
189
+ ItemTitle,
190
+ ItemDescription,
191
+ ItemHeader,
192
+ ItemFooter,
193
+ };
client/src/components/ui/kbd.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils";
2
+
3
+ function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
4
+ return (
5
+ <kbd
6
+ data-slot="kbd"
7
+ className={cn(
8
+ "bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
9
+ "[&_svg:not([class*='size-'])]:size-3",
10
+ "[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <kbd
21
+ data-slot="kbd-group"
22
+ className={cn("inline-flex items-center gap-1", className)}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ export { Kbd, KbdGroup };