ishaq101 commited on
Commit
574d44a
·
1 Parent(s): f9e0d78

[NOTICKET] Initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +3 -0
  2. .env.example +1 -0
  3. .gitignore +43 -0
  4. Dockerfile +24 -0
  5. README.md +12 -4
  6. default_shadcn_theme.css +120 -0
  7. guidelines/Guidelines.md +61 -0
  8. index.html +16 -0
  9. package-lock.json +0 -0
  10. package.json +95 -0
  11. pnpm-lock.yaml +0 -0
  12. pnpm-workspace.yaml +2 -0
  13. postcss.config.mjs +15 -0
  14. public/loading-messages.yaml +21 -0
  15. public/logo.png +0 -0
  16. server.js +85 -0
  17. src/app/App.tsx +12 -0
  18. src/app/components/KnowledgeManagement.tsx +749 -0
  19. src/app/components/Login.tsx +241 -0
  20. src/app/components/Main.tsx +847 -0
  21. src/app/components/figma/ImageWithFallback.tsx +27 -0
  22. src/app/components/ui/accordion.tsx +66 -0
  23. src/app/components/ui/alert-dialog.tsx +157 -0
  24. src/app/components/ui/alert.tsx +66 -0
  25. src/app/components/ui/aspect-ratio.tsx +11 -0
  26. src/app/components/ui/avatar.tsx +53 -0
  27. src/app/components/ui/badge.tsx +46 -0
  28. src/app/components/ui/breadcrumb.tsx +109 -0
  29. src/app/components/ui/button.tsx +58 -0
  30. src/app/components/ui/calendar.tsx +75 -0
  31. src/app/components/ui/card.tsx +92 -0
  32. src/app/components/ui/carousel.tsx +241 -0
  33. src/app/components/ui/chart.tsx +353 -0
  34. src/app/components/ui/checkbox.tsx +32 -0
  35. src/app/components/ui/collapsible.tsx +33 -0
  36. src/app/components/ui/command.tsx +177 -0
  37. src/app/components/ui/context-menu.tsx +252 -0
  38. src/app/components/ui/dialog.tsx +135 -0
  39. src/app/components/ui/drawer.tsx +132 -0
  40. src/app/components/ui/dropdown-menu.tsx +257 -0
  41. src/app/components/ui/form.tsx +168 -0
  42. src/app/components/ui/hover-card.tsx +44 -0
  43. src/app/components/ui/input-otp.tsx +77 -0
  44. src/app/components/ui/input.tsx +21 -0
  45. src/app/components/ui/label.tsx +24 -0
  46. src/app/components/ui/menubar.tsx +276 -0
  47. src/app/components/ui/navigation-menu.tsx +168 -0
  48. src/app/components/ui/pagination.tsx +127 -0
  49. src/app/components/ui/popover.tsx +48 -0
  50. src/app/components/ui/progress.tsx +31 -0
.dockerignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .git
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ VITE_API_BASE_URL=
.gitignore ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+ dist-ssr/
7
+
8
+ # Environment variables
9
+ .env
10
+ .env.local
11
+ .env.*.local
12
+
13
+ # Logs
14
+ logs/
15
+ *.log
16
+ npm-debug.log*
17
+ yarn-debug.log*
18
+ yarn-error.log*
19
+ pnpm-debug.log*
20
+
21
+ # Editor
22
+ .vscode/*
23
+ !.vscode/extensions.json
24
+ .idea/
25
+ *.suo
26
+ *.ntvs*
27
+ *.njsproj
28
+ *.sln
29
+ *.sw?
30
+
31
+ # OS
32
+ .DS_Store
33
+ Thumbs.db
34
+
35
+ # Vite
36
+ *.local
37
+ vite.config.ts.timestamp-*
38
+
39
+ API_CONTRACT.md
40
+ API_CONTRACT_VOICE.md
41
+
42
+ # Database logos (served via CDN)
43
+ public/databases/
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build
2
+ FROM node:22-alpine AS builder
3
+
4
+ RUN corepack enable && corepack prepare pnpm@latest --activate
5
+
6
+ WORKDIR /app
7
+
8
+ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml* pnpm-lock.json* ./
9
+ RUN pnpm install --no-frozen-lockfile
10
+
11
+ COPY . .
12
+ RUN pnpm build
13
+
14
+ # Stage 2: Serve
15
+ FROM node:20-alpine
16
+
17
+ WORKDIR /app
18
+
19
+ COPY --from=builder /app/dist ./dist
20
+ COPY server.js .
21
+
22
+ EXPOSE 7860
23
+
24
+ CMD ["node", "server.js"]
README.md CHANGED
@@ -1,10 +1,18 @@
1
  ---
2
- title: Demo Frontend Voice Agent
3
- emoji: 🏆
4
- colorFrom: gray
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
1
  ---
2
+ title: E2E Frontend Data Eyond
3
+ emoji: 🐠
4
+ colorFrom: green
5
+ colorTo: purple
6
  sdk: docker
7
  pinned: false
8
+ short_description: E2E Frontend Web App for Data Eyond MVP
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
12
+
13
+
14
+ ## Running the code
15
+
16
+ Run `npm i` to install the dependencies.
17
+
18
+ Run `npm run dev` to start the development server.
default_shadcn_theme.css ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* KEEP_IN_SYNC(fullscreen/resources/figmake/shadcn/globals.css) */
2
+
3
+ :root {
4
+ --font-size: 16px;
5
+ --background: #ffffff;
6
+ --foreground: oklch(0.145 0 0);
7
+ --card: #ffffff;
8
+ --card-foreground: oklch(0.145 0 0);
9
+ --popover: oklch(1 0 0);
10
+ --popover-foreground: oklch(0.145 0 0);
11
+ --primary: #030213;
12
+ --primary-foreground: oklch(1 0 0);
13
+ --secondary: oklch(0.95 0.0058 264.53);
14
+ --secondary-foreground: #030213;
15
+ --muted: #ececf0;
16
+ --muted-foreground: #717182;
17
+ --accent: #e9ebef;
18
+ --accent-foreground: #030213;
19
+ --destructive: #d4183d;
20
+ --destructive-foreground: #ffffff;
21
+ --border: rgba(0, 0, 0, 0.1);
22
+ --input: transparent;
23
+ --input-background: #f3f3f5;
24
+ --switch-background: #cbced4;
25
+ --font-weight-medium: 500;
26
+ --font-weight-normal: 400;
27
+ --ring: oklch(0.708 0 0);
28
+ --chart-1: oklch(0.646 0.222 41.116);
29
+ --chart-2: oklch(0.6 0.118 184.704);
30
+ --chart-3: oklch(0.398 0.07 227.392);
31
+ --chart-4: oklch(0.828 0.189 84.429);
32
+ --chart-5: oklch(0.769 0.188 70.08);
33
+ --radius: 0.625rem;
34
+ --sidebar: oklch(0.985 0 0);
35
+ --sidebar-foreground: oklch(0.145 0 0);
36
+ --sidebar-primary: #030213;
37
+ --sidebar-primary-foreground: oklch(0.985 0 0);
38
+ --sidebar-accent: oklch(0.97 0 0);
39
+ --sidebar-accent-foreground: oklch(0.205 0 0);
40
+ --sidebar-border: oklch(0.922 0 0);
41
+ --sidebar-ring: oklch(0.708 0 0);
42
+ }
43
+
44
+ .dark {
45
+ --background: oklch(0.145 0 0);
46
+ --foreground: oklch(0.985 0 0);
47
+ --card: oklch(0.145 0 0);
48
+ --card-foreground: oklch(0.985 0 0);
49
+ --popover: oklch(0.145 0 0);
50
+ --popover-foreground: oklch(0.985 0 0);
51
+ --primary: oklch(0.985 0 0);
52
+ --primary-foreground: oklch(0.205 0 0);
53
+ --secondary: oklch(0.269 0 0);
54
+ --secondary-foreground: oklch(0.985 0 0);
55
+ --muted: oklch(0.269 0 0);
56
+ --muted-foreground: oklch(0.708 0 0);
57
+ --accent: oklch(0.269 0 0);
58
+ --accent-foreground: oklch(0.985 0 0);
59
+ --destructive: oklch(0.396 0.141 25.723);
60
+ --destructive-foreground: oklch(0.637 0.237 25.331);
61
+ --border: oklch(0.269 0 0);
62
+ --input: oklch(0.269 0 0);
63
+ --ring: oklch(0.439 0 0);
64
+ --font-weight-medium: 500;
65
+ --font-weight-normal: 400;
66
+ --chart-1: oklch(0.488 0.243 264.376);
67
+ --chart-2: oklch(0.696 0.17 162.48);
68
+ --chart-3: oklch(0.769 0.188 70.08);
69
+ --chart-4: oklch(0.627 0.265 303.9);
70
+ --chart-5: oklch(0.645 0.246 16.439);
71
+ --sidebar: oklch(0.205 0 0);
72
+ --sidebar-foreground: oklch(0.985 0 0);
73
+ --sidebar-primary: oklch(0.488 0.243 264.376);
74
+ --sidebar-primary-foreground: oklch(0.985 0 0);
75
+ --sidebar-accent: oklch(0.269 0 0);
76
+ --sidebar-accent-foreground: oklch(0.985 0 0);
77
+ --sidebar-border: oklch(0.269 0 0);
78
+ --sidebar-ring: oklch(0.439 0 0);
79
+ }
80
+
81
+ @theme inline {
82
+ --color-background: var(--background);
83
+ --color-foreground: var(--foreground);
84
+ --color-card: var(--card);
85
+ --color-card-foreground: var(--card-foreground);
86
+ --color-popover: var(--popover);
87
+ --color-popover-foreground: var(--popover-foreground);
88
+ --color-primary: var(--primary);
89
+ --color-primary-foreground: var(--primary-foreground);
90
+ --color-secondary: var(--secondary);
91
+ --color-secondary-foreground: var(--secondary-foreground);
92
+ --color-muted: var(--muted);
93
+ --color-muted-foreground: var(--muted-foreground);
94
+ --color-accent: var(--accent);
95
+ --color-accent-foreground: var(--accent-foreground);
96
+ --color-destructive: var(--destructive);
97
+ --color-destructive-foreground: var(--destructive-foreground);
98
+ --color-border: var(--border);
99
+ --color-input: var(--input);
100
+ --color-input-background: var(--input-background);
101
+ --color-switch-background: var(--switch-background);
102
+ --color-ring: var(--ring);
103
+ --color-chart-1: var(--chart-1);
104
+ --color-chart-2: var(--chart-2);
105
+ --color-chart-3: var(--chart-3);
106
+ --color-chart-4: var(--chart-4);
107
+ --color-chart-5: var(--chart-5);
108
+ --radius-sm: calc(var(--radius) - 4px);
109
+ --radius-md: calc(var(--radius) - 2px);
110
+ --radius-lg: var(--radius);
111
+ --radius-xl: calc(var(--radius) + 4px);
112
+ --color-sidebar: var(--sidebar);
113
+ --color-sidebar-foreground: var(--sidebar-foreground);
114
+ --color-sidebar-primary: var(--sidebar-primary);
115
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
116
+ --color-sidebar-accent: var(--sidebar-accent);
117
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
118
+ --color-sidebar-border: var(--sidebar-border);
119
+ --color-sidebar-ring: var(--sidebar-ring);
120
+ }
guidelines/Guidelines.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ **Add your own guidelines here**
2
+ <!--
3
+
4
+ System Guidelines
5
+
6
+ Use this file to provide the AI with rules and guidelines you want it to follow.
7
+ This template outlines a few examples of things you can add. You can add your own sections and format it to suit your needs
8
+
9
+ TIP: More context isn't always better. It can confuse the LLM. Try and add the most important rules you need
10
+
11
+ # General guidelines
12
+
13
+ Any general rules you want the AI to follow.
14
+ For example:
15
+
16
+ * Only use absolute positioning when necessary. Opt for responsive and well structured layouts that use flexbox and grid by default
17
+ * Refactor code as you go to keep code clean
18
+ * Keep file sizes small and put helper functions and components in their own files.
19
+
20
+ --------------
21
+
22
+ # Design system guidelines
23
+ Rules for how the AI should make generations look like your company's design system
24
+
25
+ Additionally, if you select a design system to use in the prompt box, you can reference
26
+ your design system's components, tokens, variables and components.
27
+ For example:
28
+
29
+ * Use a base font-size of 14px
30
+ * Date formats should always be in the format “Jun 10”
31
+ * The bottom toolbar should only ever have a maximum of 4 items
32
+ * Never use the floating action button with the bottom toolbar
33
+ * Chips should always come in sets of 3 or more
34
+ * Don't use a dropdown if there are 2 or fewer options
35
+
36
+ You can also create sub sections and add more specific details
37
+ For example:
38
+
39
+
40
+ ## Button
41
+ The Button component is a fundamental interactive element in our design system, designed to trigger actions or navigate
42
+ users through the application. It provides visual feedback and clear affordances to enhance user experience.
43
+
44
+ ### Usage
45
+ Buttons should be used for important actions that users need to take, such as form submissions, confirming choices,
46
+ or initiating processes. They communicate interactivity and should have clear, action-oriented labels.
47
+
48
+ ### Variants
49
+ * Primary Button
50
+ * Purpose : Used for the main action in a section or page
51
+ * Visual Style : Bold, filled with the primary brand color
52
+ * Usage : One primary button per section to guide users toward the most important action
53
+ * Secondary Button
54
+ * Purpose : Used for alternative or supporting actions
55
+ * Visual Style : Outlined with the primary color, transparent background
56
+ * Usage : Can appear alongside a primary button for less important actions
57
+ * Tertiary Button
58
+ * Purpose : Used for the least important actions
59
+ * Visual Style : Text-only with no border, using primary color
60
+ * Usage : For actions that should be available but not emphasized
61
+ -->
index.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="icon" type="image/png" href="/logo.png" />
8
+ <title>Chatbot application</title>
9
+ </head>
10
+
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/src/main.tsx"></script>
14
+ </body>
15
+ </html>
16
+
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@figma/my-make-file",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "vite build",
8
+ "dev": "vite"
9
+ },
10
+ "dependencies": {
11
+ "@emotion/react": "11.14.0",
12
+ "@emotion/styled": "11.14.1",
13
+ "@mui/icons-material": "7.3.5",
14
+ "@mui/material": "7.3.5",
15
+ "@popperjs/core": "2.11.8",
16
+ "@radix-ui/react-accordion": "1.2.3",
17
+ "@radix-ui/react-alert-dialog": "1.1.6",
18
+ "@radix-ui/react-aspect-ratio": "1.1.2",
19
+ "@radix-ui/react-avatar": "1.1.3",
20
+ "@radix-ui/react-checkbox": "1.1.4",
21
+ "@radix-ui/react-collapsible": "1.1.3",
22
+ "@radix-ui/react-context-menu": "2.2.6",
23
+ "@radix-ui/react-dialog": "1.1.6",
24
+ "@radix-ui/react-dropdown-menu": "2.1.6",
25
+ "@radix-ui/react-hover-card": "1.1.6",
26
+ "@radix-ui/react-label": "2.1.2",
27
+ "@radix-ui/react-menubar": "1.1.6",
28
+ "@radix-ui/react-navigation-menu": "1.2.5",
29
+ "@radix-ui/react-popover": "1.1.6",
30
+ "@radix-ui/react-progress": "1.1.2",
31
+ "@radix-ui/react-radio-group": "1.2.3",
32
+ "@radix-ui/react-scroll-area": "1.2.3",
33
+ "@radix-ui/react-select": "2.1.6",
34
+ "@radix-ui/react-separator": "1.1.2",
35
+ "@radix-ui/react-slider": "1.2.3",
36
+ "@radix-ui/react-slot": "1.1.2",
37
+ "@radix-ui/react-switch": "1.1.3",
38
+ "@radix-ui/react-tabs": "1.1.3",
39
+ "@radix-ui/react-toggle": "1.1.2",
40
+ "@radix-ui/react-toggle-group": "1.1.2",
41
+ "@radix-ui/react-tooltip": "1.1.8",
42
+ "canvas-confetti": "1.9.4",
43
+ "class-variance-authority": "0.7.1",
44
+ "clsx": "2.1.1",
45
+ "cmdk": "1.1.1",
46
+ "date-fns": "3.6.0",
47
+ "embla-carousel-react": "8.6.0",
48
+ "input-otp": "1.4.2",
49
+ "katex": "^0.16.45",
50
+ "lucide-react": "0.487.0",
51
+ "motion": "12.23.24",
52
+ "next-themes": "0.4.6",
53
+ "react-day-picker": "8.10.1",
54
+ "react-dnd": "16.0.1",
55
+ "react-dnd-html5-backend": "16.0.1",
56
+ "react-hook-form": "7.55.0",
57
+ "react-markdown": "^10.1.0",
58
+ "react-popper": "2.3.0",
59
+ "react-resizable-panels": "2.1.7",
60
+ "react-responsive-masonry": "2.7.1",
61
+ "react-router": "7.13.0",
62
+ "react-slick": "0.31.0",
63
+ "recharts": "2.15.2",
64
+ "rehype-katex": "^7.0.1",
65
+ "remark-gfm": "^4.0.1",
66
+ "remark-math": "^6.0.0",
67
+ "sonner": "2.0.3",
68
+ "tailwind-merge": "3.2.0",
69
+ "tw-animate-css": "1.3.8",
70
+ "vaul": "1.1.2"
71
+ },
72
+ "devDependencies": {
73
+ "@tailwindcss/vite": "4.1.12",
74
+ "@vitejs/plugin-react": "4.7.0",
75
+ "tailwindcss": "4.1.12",
76
+ "vite": "6.3.5"
77
+ },
78
+ "peerDependencies": {
79
+ "react": "18.3.1",
80
+ "react-dom": "18.3.1"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "react": {
84
+ "optional": true
85
+ },
86
+ "react-dom": {
87
+ "optional": true
88
+ }
89
+ },
90
+ "pnpm": {
91
+ "overrides": {
92
+ "vite": "6.3.5"
93
+ }
94
+ }
95
+ }
pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
pnpm-workspace.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ packages:
2
+ - '.'
postcss.config.mjs ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * PostCSS Configuration
3
+ *
4
+ * Tailwind CSS v4 (via @tailwindcss/vite) automatically sets up all required
5
+ * PostCSS plugins — you do NOT need to include `tailwindcss` or `autoprefixer` here.
6
+ *
7
+ * This file only exists for adding additional PostCSS plugins, if needed.
8
+ * For example:
9
+ *
10
+ * import postcssNested from 'postcss-nested'
11
+ * export default { plugins: [postcssNested()] }
12
+ *
13
+ * Otherwise, you can leave this file empty.
14
+ */
15
+ export default {}
public/loading-messages.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ messages:
2
+ - Routing
3
+ - Parsing
4
+ - Mapping
5
+ - Tracing
6
+ - Linking
7
+ - Binding
8
+ - Buffering
9
+ - Merging
10
+ - Resolving
11
+ - Scanning
12
+ - Collating
13
+ - Filtering
14
+ - Balancing
15
+ - Shaping
16
+ - Aligning
17
+ - Weaving
18
+ - Framing
19
+ - Tuning
20
+ - Grounding
21
+ - Composing
public/logo.png ADDED
server.js ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const http = require("http");
2
+ const https = require("https");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const url = require("url");
6
+
7
+ const PORT = 7860;
8
+ const DIST_DIR = path.join(__dirname, "dist");
9
+ const BACKEND_URL = process.env.VITE_API_BASE_URL || "";
10
+
11
+ const MIME = {
12
+ ".html": "text/html",
13
+ ".js": "application/javascript",
14
+ ".mjs": "application/javascript",
15
+ ".css": "text/css",
16
+ ".json": "application/json",
17
+ ".png": "image/png",
18
+ ".jpg": "image/jpeg",
19
+ ".jpeg": "image/jpeg",
20
+ ".svg": "image/svg+xml",
21
+ ".ico": "image/x-icon",
22
+ ".woff": "font/woff",
23
+ ".woff2": "font/woff2",
24
+ ".ttf": "font/ttf",
25
+ ".webp": "image/webp",
26
+ };
27
+
28
+ console.log(`Starting server on port ${PORT}`);
29
+ console.log(`Backend URL: ${BACKEND_URL || "(not set)"}`);
30
+
31
+ const server = http.createServer((req, res) => {
32
+ const parsed = url.parse(req.url);
33
+
34
+ // Proxy /api/* to backend
35
+ if (parsed.pathname.startsWith("/api/") && BACKEND_URL) {
36
+ const backend = url.parse(BACKEND_URL);
37
+ const isHttps = backend.protocol === "https:";
38
+ const transport = isHttps ? https : http;
39
+
40
+ const options = {
41
+ hostname: backend.hostname,
42
+ port: backend.port || (isHttps ? 443 : 80),
43
+ path: req.url,
44
+ method: req.method,
45
+ headers: { ...req.headers, host: backend.hostname },
46
+ };
47
+
48
+ const proxyReq = transport.request(options, (proxyRes) => {
49
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
50
+ proxyRes.pipe(res);
51
+ });
52
+
53
+ proxyReq.on("error", (err) => {
54
+ console.error("Proxy error:", err.message);
55
+ res.writeHead(502);
56
+ res.end("Bad Gateway");
57
+ });
58
+
59
+ req.pipe(proxyReq);
60
+ return;
61
+ }
62
+
63
+ // Serve static files
64
+ let filePath = path.join(DIST_DIR, parsed.pathname);
65
+ try {
66
+ const stat = fs.statSync(filePath);
67
+ if (stat.isDirectory()) filePath = path.join(filePath, "index.html");
68
+ } catch {
69
+ // Not found → SPA fallback
70
+ filePath = path.join(DIST_DIR, "index.html");
71
+ }
72
+
73
+ fs.readFile(filePath, (err, data) => {
74
+ if (err) {
75
+ res.writeHead(404);
76
+ res.end("Not Found");
77
+ return;
78
+ }
79
+ const ext = path.extname(filePath).toLowerCase();
80
+ res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" });
81
+ res.end(data);
82
+ });
83
+ });
84
+
85
+ server.listen(PORT);
src/app/App.tsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RouterProvider } from "react-router";
2
+ import { router } from "./routes";
3
+ import { Toaster } from "./components/ui/sonner";
4
+
5
+ export default function App() {
6
+ return (
7
+ <>
8
+ <RouterProvider router={router} />
9
+ <Toaster richColors position="top-right" />
10
+ </>
11
+ );
12
+ }
src/app/components/KnowledgeManagement.tsx ADDED
@@ -0,0 +1,749 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react";
2
+ import {
3
+ Upload,
4
+ Trash2,
5
+ FileText,
6
+ Check,
7
+ Loader2,
8
+ Database,
9
+ X,
10
+ ChevronLeft,
11
+ Link,
12
+ } from "lucide-react";
13
+ import { toast } from "sonner";
14
+ import {
15
+ getDocuments,
16
+ uploadDocument,
17
+ processDocument,
18
+ deleteDocument,
19
+ getDocumentTypes,
20
+ getDatabaseClientTypes,
21
+ connectDatabase,
22
+ getDatabaseClients,
23
+ deleteDatabaseClient,
24
+ ingestDatabaseClient,
25
+ type ApiDocument,
26
+ type DocumentStatus,
27
+ type DocTypeInfo,
28
+ type DbType,
29
+ type DbTypeInfo,
30
+ type DatabaseClient,
31
+ } from "../../services/api";
32
+
33
+ interface KnowledgeManagementProps {
34
+ open: boolean;
35
+ onClose: () => void;
36
+ }
37
+
38
+ type View = "main" | "db-select" | "db-credentials";
39
+
40
+ const LOGO_MAP: Record<string, string> = {
41
+ postgres: "https://cdn.simpleicons.org/postgresql/336791",
42
+ mysql: "https://cdn.simpleicons.org/mysql/4479A1",
43
+ supabase: "https://cdn.simpleicons.org/supabase/3ECF8E",
44
+ sqlserver: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/microsoftsqlserver/microsoftsqlserver-plain.svg",
45
+ bigquery: "https://cdn.simpleicons.org/googlebigquery/4285F4",
46
+ snowflake: "https://cdn.simpleicons.org/snowflake/29B5E8",
47
+ };
48
+
49
+ const getUserId = (): string | null => {
50
+ const stored = localStorage.getItem("chatbot_user");
51
+ if (!stored) return null;
52
+ return (JSON.parse(stored).user_id as string) ?? null;
53
+ };
54
+
55
+ export default function KnowledgeManagement({
56
+ open,
57
+ onClose,
58
+ }: KnowledgeManagementProps) {
59
+ // ── Document state ──────────────────────────────────────────────────────────
60
+ const [docTypes, setDocTypes] = useState<DocTypeInfo[]>([]);
61
+ const [documents, setDocuments] = useState<ApiDocument[]>([]);
62
+ const [loadingDocs, setLoadingDocs] = useState(false);
63
+ const [docsError, setDocsError] = useState<string | null>(null);
64
+ const [uploading, setUploading] = useState(false);
65
+ const [uploadError, setUploadError] = useState<string | null>(null);
66
+ const [processing, setProcessing] = useState<string | null>(null);
67
+ const [deleting, setDeleting] = useState<string | null>(null);
68
+
69
+ // ── Navigation state ────────────────────────────────────────────────────────
70
+ const [view, setView] = useState<View>("main");
71
+ const [selectedDbType, setSelectedDbType] = useState<DbType | null>(null);
72
+
73
+ // ── DB type & client state ──────────────────────────────────────────────────
74
+ const [dbTypeInfos, setDbTypeInfos] = useState<DbTypeInfo[]>([]);
75
+ const [dbClients, setDbClients] = useState<DatabaseClient[]>([]);
76
+ const [loadingDbTypes, setLoadingDbTypes] = useState(false);
77
+ const [ingesting, setIngesting] = useState<string | null>(null);
78
+ const [deletingClient, setDeletingClient] = useState<string | null>(null);
79
+
80
+ // ── DB credentials form state ───────────────────────────────────────────────
81
+ const [connectionName, setConnectionName] = useState("");
82
+ const [dbForm, setDbForm] = useState<Record<string, string | number | boolean>>({});
83
+ const [connecting, setConnecting] = useState(false);
84
+
85
+ useEffect(() => {
86
+ if (!open) return;
87
+ const userId = getUserId();
88
+ if (!userId) return;
89
+ loadDocuments(userId);
90
+ loadDbData(userId);
91
+ getDocumentTypes()
92
+ .then((types) => setDocTypes(types.filter((t) => t.status === "active")))
93
+ .catch(() =>
94
+ setDocTypes([
95
+ { doc_type: "pdf", max_size: 10, status: "active", message: null },
96
+ { doc_type: "csv", max_size: 10, status: "active", message: null },
97
+ { doc_type: "xlsx", max_size: 10, status: "active", message: null },
98
+ ])
99
+ );
100
+ }, [open]);
101
+
102
+ const acceptedExtensions = docTypes.map((t) => `.${t.doc_type}`).join(",");
103
+ const supportedFormatsText = docTypes.map((t) => t.doc_type.toUpperCase()).join(", ");
104
+
105
+ const loadDbData = async (userId: string) => {
106
+ setLoadingDbTypes(true);
107
+ try {
108
+ const [types, clients] = await Promise.all([
109
+ getDatabaseClientTypes(),
110
+ getDatabaseClients(userId),
111
+ ]);
112
+ setDbTypeInfos(types);
113
+ setDbClients(clients);
114
+ } catch {
115
+ // non-blocking; silently fail
116
+ } finally {
117
+ setLoadingDbTypes(false);
118
+ }
119
+ };
120
+
121
+ const handleClose = () => {
122
+ setView("main");
123
+ setSelectedDbType(null);
124
+ setConnectionName("");
125
+ setDbForm({});
126
+ onClose();
127
+ };
128
+
129
+ // ── Document handlers ───────────────────────────────────────────────────────
130
+
131
+ const loadDocuments = async (userId: string) => {
132
+ setLoadingDocs(true);
133
+ setDocsError(null);
134
+ try {
135
+ setDocuments(await getDocuments(userId));
136
+ } catch (err) {
137
+ setDocsError(
138
+ err instanceof Error ? err.message : "Failed to load documents"
139
+ );
140
+ } finally {
141
+ setLoadingDocs(false);
142
+ }
143
+ };
144
+
145
+ const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
146
+ const files = e.target.files;
147
+ if (!files || files.length === 0) return;
148
+ const userId = getUserId();
149
+ if (!userId) return;
150
+
151
+ setUploading(true);
152
+ setUploadError(null);
153
+
154
+ for (let i = 0; i < files.length; i++) {
155
+ const file = files[i];
156
+ try {
157
+ const uploadRes = await uploadDocument(userId, file);
158
+ const newDoc: ApiDocument = {
159
+ id: uploadRes.data.id,
160
+ filename: uploadRes.data.filename,
161
+ status: "pending",
162
+ file_size: file.size,
163
+ file_type: file.name.split(".").pop() ?? "",
164
+ created_at: new Date().toISOString(),
165
+ };
166
+ setDocuments((prev) => [newDoc, ...prev]);
167
+ await processDocumentById(userId, uploadRes.data.id);
168
+ } catch (err) {
169
+ setUploadError(err instanceof Error ? err.message : "Upload failed");
170
+ }
171
+ }
172
+
173
+ setUploading(false);
174
+ e.target.value = "";
175
+ };
176
+
177
+ const processDocumentById = async (userId: string, docId: string) => {
178
+ setProcessing(docId);
179
+ setDocuments((prev) =>
180
+ prev.map((d) =>
181
+ d.id === docId ? { ...d, status: "processing" as DocumentStatus } : d
182
+ )
183
+ );
184
+ try {
185
+ await processDocument(userId, docId);
186
+ setDocuments((prev) =>
187
+ prev.map((d) =>
188
+ d.id === docId ? { ...d, status: "completed" as DocumentStatus } : d
189
+ )
190
+ );
191
+ } catch {
192
+ setDocuments((prev) =>
193
+ prev.map((d) =>
194
+ d.id === docId ? { ...d, status: "failed" as DocumentStatus } : d
195
+ )
196
+ );
197
+ } finally {
198
+ setProcessing(null);
199
+ }
200
+ };
201
+
202
+ const handleDeleteDocument = async (docId: string) => {
203
+ const userId = getUserId();
204
+ if (!userId) return;
205
+ setDeleting(docId);
206
+ try {
207
+ await deleteDocument(userId, docId);
208
+ setDocuments((prev) => prev.filter((d) => d.id !== docId));
209
+ } catch (err) {
210
+ console.error("Delete failed:", err);
211
+ } finally {
212
+ setDeleting(null);
213
+ }
214
+ };
215
+
216
+ const deleteAllDocuments = async () => {
217
+ if (!window.confirm("Are you sure you want to delete all documents?"))
218
+ return;
219
+ const userId = getUserId();
220
+ if (!userId) return;
221
+ for (const doc of documents) {
222
+ try {
223
+ await deleteDocument(userId, doc.id);
224
+ } catch {
225
+ // continue deleting others
226
+ }
227
+ }
228
+ setDocuments([]);
229
+ };
230
+
231
+ // ── DB handlers ─────────────────────────────────────────────────────────────
232
+
233
+ const handleDbConnect = async () => {
234
+ const userId = getUserId();
235
+ if (!userId || !selectedDbType || !connectionName.trim()) return;
236
+ setConnecting(true);
237
+ try {
238
+ await connectDatabase(userId, selectedDbType, connectionName.trim(), dbForm);
239
+ const clients = await getDatabaseClients(userId);
240
+ setDbClients(clients);
241
+ toast.success("Database connected successfully");
242
+ setView("main");
243
+ setConnectionName("");
244
+ setDbForm({});
245
+ } catch (err) {
246
+ toast.error(
247
+ err instanceof Error ? err.message : "Failed to connect to database"
248
+ );
249
+ } finally {
250
+ setConnecting(false);
251
+ }
252
+ };
253
+
254
+ const handleIngest = async (clientId: string) => {
255
+ const userId = getUserId();
256
+ if (!userId) return;
257
+ setIngesting(clientId);
258
+ try {
259
+ const res = await ingestDatabaseClient(clientId, userId);
260
+ toast.success(`Ingested ${res.chunks_ingested} chunks successfully`);
261
+ } catch (err) {
262
+ toast.error(err instanceof Error ? err.message : "Ingestion failed");
263
+ } finally {
264
+ setIngesting(null);
265
+ }
266
+ };
267
+
268
+ const handleDeleteClient = async (clientId: string) => {
269
+ const userId = getUserId();
270
+ if (!userId) return;
271
+ setDeletingClient(clientId);
272
+ try {
273
+ await deleteDatabaseClient(clientId, userId);
274
+ setDbClients((prev) => prev.filter((c) => c.id !== clientId));
275
+ } catch (err) {
276
+ toast.error(err instanceof Error ? err.message : "Failed to delete connection");
277
+ } finally {
278
+ setDeletingClient(null);
279
+ }
280
+ };
281
+
282
+ // ── Helpers ─────────────────────────────────────────────────────────────────
283
+
284
+ const formatFileSize = (bytes: number) => {
285
+ if (bytes < 1024) return bytes + " B";
286
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
287
+ return (bytes / (1024 * 1024)).toFixed(2) + " MB";
288
+ };
289
+
290
+ const formatDate = (isoString: string) => {
291
+ return new Date(isoString).toLocaleString();
292
+ };
293
+
294
+ const renderStatus = (doc: ApiDocument) => {
295
+ if (doc.status === "completed") {
296
+ return (
297
+ <div className="flex items-center gap-1.5 text-green-600">
298
+ <Check className="w-3.5 h-3.5" />
299
+ <span className="text-xs font-medium">Processed</span>
300
+ </div>
301
+ );
302
+ }
303
+
304
+ if (doc.status === "processing" || processing === doc.id) {
305
+ return (
306
+ <div className="flex items-center gap-1.5 text-blue-600">
307
+ <Loader2 className="w-3.5 h-3.5 animate-spin" />
308
+ <span className="text-xs">Processing...</span>
309
+ </div>
310
+ );
311
+ }
312
+
313
+ return (
314
+ <button
315
+ onClick={() => {
316
+ const userId = getUserId();
317
+ if (userId) processDocumentById(userId, doc.id);
318
+ }}
319
+ disabled={processing === doc.id}
320
+ className="flex items-center gap-1.5 bg-gradient-to-r from-[#00C853] to-[#00A843] text-white px-3 py-1.5 rounded-lg hover:from-[#00A843] hover:to-[#00962B] transition disabled:opacity-50 disabled:cursor-not-allowed text-xs"
321
+ >
322
+ <Database className="w-3.5 h-3.5" />
323
+ {doc.status === "failed" ? "Retry Process" : "Process to Knowledge"}
324
+ </button>
325
+ );
326
+ };
327
+
328
+ if (!open) return null;
329
+
330
+ // ── Header title & back button logic ────────────────────────────────────────
331
+
332
+ const selectedDbInfo = dbTypeInfos.find((d) => d.db_type === selectedDbType);
333
+
334
+ const headerTitle =
335
+ view === "db-select"
336
+ ? "Connect Database"
337
+ : view === "db-credentials"
338
+ ? `Connect to ${selectedDbInfo?.display_name ?? selectedDbType}`
339
+ : "Knowledge Base";
340
+
341
+ const headerBack =
342
+ view === "db-select"
343
+ ? () => setView("main")
344
+ : view === "db-credentials"
345
+ ? () => setView("db-select")
346
+ : null;
347
+
348
+ // ── Render ───────────────────────────────────────────────────────────────────
349
+
350
+ return (
351
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
352
+ <div className="bg-white rounded-2xl shadow-xl w-full max-w-md max-h-[85vh] flex flex-col">
353
+
354
+ {/* Header */}
355
+ <div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
356
+ <div className="flex items-center gap-3">
357
+ {headerBack ? (
358
+ <button
359
+ onClick={headerBack}
360
+ className="p-1.5 -ml-1 rounded-lg text-slate-400 hover:text-slate-700 hover:bg-slate-100 transition"
361
+ aria-label="Back"
362
+ >
363
+ <ChevronLeft className="w-4 h-4" />
364
+ </button>
365
+ ) : (
366
+ <div className="w-7 h-7 rounded-lg bg-orange-100 flex items-center justify-center">
367
+ <Database className="w-4 h-4 text-[#FF8F00]" />
368
+ </div>
369
+ )}
370
+ <h2 className="text-sm font-semibold text-slate-900">{headerTitle}</h2>
371
+ </div>
372
+ <button
373
+ onClick={handleClose}
374
+ className="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 hover:bg-slate-100 transition"
375
+ >
376
+ <X className="w-4 h-4" />
377
+ </button>
378
+ </div>
379
+
380
+ {/* Content */}
381
+ <div className="flex-1 overflow-y-auto px-5 py-4">
382
+
383
+ {/* ── VIEW: main ── */}
384
+ {view === "main" && (
385
+ <div className="space-y-4">
386
+
387
+ {/* Upload zone */}
388
+ <label
389
+ htmlFor="file-upload"
390
+ className="group flex flex-col items-center gap-2.5 border-2 border-dashed border-slate-200 rounded-xl p-7 cursor-pointer hover:border-[#FF8F00] hover:bg-orange-50/30 transition-colors"
391
+ >
392
+ <div className="w-10 h-10 rounded-xl bg-slate-100 group-hover:bg-orange-100 flex items-center justify-center transition-colors">
393
+ {uploading ? (
394
+ <Loader2 className="w-5 h-5 text-[#FF8F00] animate-spin" />
395
+ ) : (
396
+ <Upload className="w-5 h-5 text-slate-400 group-hover:text-[#FF8F00] transition-colors" />
397
+ )}
398
+ </div>
399
+ <div className="text-center">
400
+ <p className="text-sm font-medium text-slate-700">
401
+ {uploading ? "Uploading…" : (
402
+ <>Drop files, or <span className="text-[#FF8F00]">browse</span></>
403
+ )}
404
+ </p>
405
+ <p className="text-xs text-slate-400 mt-0.5">{supportedFormatsText}</p>
406
+ </div>
407
+ <input
408
+ id="file-upload"
409
+ type="file"
410
+ accept={acceptedExtensions}
411
+ multiple
412
+ onChange={handleFileUpload}
413
+ className="hidden"
414
+ disabled={uploading}
415
+ />
416
+ </label>
417
+
418
+ {uploadError && (
419
+ <p className="text-xs text-red-500 bg-red-50 border border-red-100 px-3 py-2 rounded-lg">
420
+ {uploadError}
421
+ </p>
422
+ )}
423
+
424
+ {/* Connect DB row */}
425
+ <button
426
+ onClick={() => setView("db-select")}
427
+ className="w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-slate-200 hover:border-slate-300 hover:bg-slate-50 transition text-left group"
428
+ >
429
+ <div className="w-8 h-8 rounded-lg bg-slate-100 group-hover:bg-slate-200 flex items-center justify-center flex-shrink-0 transition-colors">
430
+ <Link className="w-4 h-4 text-slate-500" />
431
+ </div>
432
+ <div className="flex-1 min-w-0">
433
+ <p className="text-sm font-medium text-slate-700">Connect a Database</p>
434
+ <p className="text-xs text-slate-400">PostgreSQL and more</p>
435
+ </div>
436
+ <ChevronLeft className="w-4 h-4 text-slate-300 rotate-180 flex-shrink-0" />
437
+ </button>
438
+
439
+ {/* Database connections list */}
440
+ {dbClients.length > 0 && (
441
+ <div>
442
+ <div className="flex items-center justify-between mb-2">
443
+ <span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
444
+ Databases · {dbClients.length}
445
+ </span>
446
+ </div>
447
+ <div className="space-y-1">
448
+ {dbClients.map((client) => (
449
+ <div
450
+ key={client.id}
451
+ className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-slate-50 transition group"
452
+ >
453
+ <div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center flex-shrink-0">
454
+ <img
455
+ src={LOGO_MAP[client.db_type] ?? ""}
456
+ alt={client.db_type}
457
+ className="w-5 h-5 object-contain"
458
+ />
459
+ </div>
460
+ <div className="flex-1 min-w-0">
461
+ <p className="text-sm font-medium text-slate-800 truncate">{client.name}</p>
462
+ <p className="text-xs text-slate-400 capitalize">{client.db_type} · {client.status}</p>
463
+ </div>
464
+ <div className="flex items-center gap-1.5 flex-shrink-0">
465
+ <button
466
+ onClick={() => handleIngest(client.id)}
467
+ disabled={ingesting === client.id || client.status === "inactive"}
468
+ title="Ingest schema to knowledge base"
469
+ className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-slate-100 hover:bg-orange-100 hover:text-[#FF8F00] text-slate-500 transition disabled:opacity-40 disabled:cursor-not-allowed"
470
+ >
471
+ {ingesting === client.id ? (
472
+ <Loader2 className="w-3 h-3 animate-spin" />
473
+ ) : (
474
+ <Database className="w-3 h-3" />
475
+ )}
476
+ {ingesting === client.id ? "Ingesting…" : "Ingest"}
477
+ </button>
478
+ <button
479
+ onClick={() => handleDeleteClient(client.id)}
480
+ disabled={deletingClient === client.id}
481
+ className="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition disabled:opacity-30"
482
+ title="Delete connection"
483
+ >
484
+ {deletingClient === client.id ? (
485
+ <Loader2 className="w-3.5 h-3.5 animate-spin" />
486
+ ) : (
487
+ <Trash2 className="w-3.5 h-3.5" />
488
+ )}
489
+ </button>
490
+ </div>
491
+ </div>
492
+ ))}
493
+ </div>
494
+ </div>
495
+ )}
496
+
497
+ {/* Documents list */}
498
+ <div>
499
+ <div className="flex items-center justify-between mb-2">
500
+ <span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
501
+ Documents · {documents.length}
502
+ </span>
503
+ {documents.length > 0 && (
504
+ <button
505
+ onClick={deleteAllDocuments}
506
+ className="text-xs text-slate-400 hover:text-red-500 flex items-center gap-1 transition"
507
+ >
508
+ <Trash2 className="w-3 h-3" />
509
+ Clear all
510
+ </button>
511
+ )}
512
+ </div>
513
+
514
+ {loadingDocs ? (
515
+ <div className="flex justify-center py-10">
516
+ <Loader2 className="w-5 h-5 animate-spin text-slate-300" />
517
+ </div>
518
+ ) : docsError ? (
519
+ <p className="text-center text-xs text-red-500 py-6">{docsError}</p>
520
+ ) : documents.length === 0 ? (
521
+ <div className="text-center py-10">
522
+ <div className="w-12 h-12 rounded-2xl bg-slate-100 flex items-center justify-center mx-auto mb-3">
523
+ <FileText className="w-5 h-5 text-slate-300" />
524
+ </div>
525
+ <p className="text-sm text-slate-400">No documents yet</p>
526
+ <p className="text-xs text-slate-300 mt-0.5">Upload files to get started</p>
527
+ </div>
528
+ ) : (
529
+ <div className="space-y-1">
530
+ {documents.map((doc) => (
531
+ <div
532
+ key={doc.id}
533
+ className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-slate-50 transition group"
534
+ >
535
+ <div className="w-8 h-8 rounded-lg bg-red-50 flex items-center justify-center flex-shrink-0">
536
+ <FileText className="w-4 h-4 text-red-400" />
537
+ </div>
538
+ <div className="flex-1 min-w-0">
539
+ <p className="text-sm font-medium text-slate-800 truncate" title={doc.filename}>
540
+ {doc.filename}
541
+ </p>
542
+ <p className="text-xs text-slate-400">
543
+ {formatFileSize(doc.file_size)} · {formatDate(doc.created_at)}
544
+ </p>
545
+ </div>
546
+ <div className="flex items-center gap-2 flex-shrink-0">
547
+ {renderStatus(doc)}
548
+ <button
549
+ onClick={() => handleDeleteDocument(doc.id)}
550
+ disabled={deleting === doc.id}
551
+ className="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition disabled:opacity-30"
552
+ title="Delete"
553
+ >
554
+ {deleting === doc.id ? (
555
+ <Loader2 className="w-3.5 h-3.5 animate-spin" />
556
+ ) : (
557
+ <Trash2 className="w-3.5 h-3.5" />
558
+ )}
559
+ </button>
560
+ </div>
561
+ </div>
562
+ ))}
563
+ </div>
564
+ )}
565
+ </div>
566
+ </div>
567
+ )}
568
+
569
+ {/* ── VIEW: db-select ── */}
570
+ {view === "db-select" && (
571
+ <div className="space-y-4">
572
+ <p className="text-sm text-slate-400">
573
+ Choose a database to connect to your knowledge base.
574
+ </p>
575
+ {loadingDbTypes ? (
576
+ <div className="flex justify-center py-10">
577
+ <Loader2 className="w-5 h-5 animate-spin text-slate-300" />
578
+ </div>
579
+ ) : (
580
+ <div className="grid grid-cols-3 gap-2">
581
+ {dbTypeInfos.map((info) => {
582
+ const active = info.status === "active";
583
+ return (
584
+ <button
585
+ key={info.db_type}
586
+ disabled={!active}
587
+ onClick={() => {
588
+ if (!active) return;
589
+ setSelectedDbType(info.db_type);
590
+ const defaults = Object.fromEntries(
591
+ info.fields.map((f) => [
592
+ f.name,
593
+ f.default != null
594
+ ? f.default
595
+ : f.type === "integer" ? 0 : f.type === "boolean" ? false : "",
596
+ ])
597
+ );
598
+ setDbForm(defaults);
599
+ setView("db-credentials");
600
+ }}
601
+ className={[
602
+ "relative flex flex-col items-center gap-2 p-4 rounded-xl border transition text-xs font-medium",
603
+ active
604
+ ? "border-slate-200 hover:border-[#FF8F00] hover:bg-orange-50/50 text-slate-700 cursor-pointer"
605
+ : "border-slate-100 bg-slate-50/60 text-slate-400 cursor-not-allowed",
606
+ ].join(" ")}
607
+ >
608
+ <img
609
+ src={LOGO_MAP[info.logo] ?? ""}
610
+ alt={info.display_name}
611
+ className={`w-8 h-8 object-contain ${!active ? "opacity-30 grayscale" : ""}`}
612
+ />
613
+ <span>{info.display_name}</span>
614
+ {!active && (
615
+ <span className="absolute top-1.5 right-1.5 text-[9px] bg-slate-200 text-slate-400 px-1.5 py-0.5 rounded-full">
616
+ Soon
617
+ </span>
618
+ )}
619
+ </button>
620
+ );
621
+ })}
622
+ </div>
623
+ )}
624
+ </div>
625
+ )}
626
+
627
+ {/* ── VIEW: db-credentials ── */}
628
+ {view === "db-credentials" && (
629
+ <div className="space-y-4">
630
+
631
+ {/* Selected DB chip */}
632
+ {selectedDbInfo && (
633
+ <div className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl bg-slate-50 border border-slate-100">
634
+ <img src={LOGO_MAP[selectedDbInfo.logo] ?? ""} alt={selectedDbInfo.display_name} className="w-5 h-5 object-contain" />
635
+ <span className="text-sm font-medium text-slate-700">{selectedDbInfo.display_name}</span>
636
+ </div>
637
+ )}
638
+
639
+ <div className="grid grid-cols-2 gap-3">
640
+ {/* Connection Name */}
641
+ <div className="col-span-2">
642
+ <label className="block text-xs font-medium text-slate-500 mb-1">
643
+ Connection Name <span className="text-red-400">*</span>
644
+ </label>
645
+ <input
646
+ type="text"
647
+ placeholder="Production DB"
648
+ value={connectionName}
649
+ onChange={(e) => setConnectionName(e.target.value)}
650
+ className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] placeholder:text-slate-300 transition"
651
+ />
652
+ </div>
653
+
654
+ {/* Dynamic fields from API */}
655
+ {selectedDbInfo?.fields.map((field) => (
656
+ <div
657
+ key={field.name}
658
+ className={field.name === "host" || field.name === "service_account_json" ? "col-span-2" : ""}
659
+ >
660
+ <label className="block text-xs font-medium text-slate-500 mb-1 capitalize">
661
+ {field.name.replace(/_/g, " ")}
662
+ {field.required && <span className="text-red-400 ml-0.5">*</span>}
663
+ </label>
664
+
665
+ {field.type === "select" ? (
666
+ <select
667
+ value={String(dbForm[field.name] ?? field.default ?? "")}
668
+ onChange={(e) => setDbForm((f) => ({ ...f, [field.name]: e.target.value }))}
669
+ className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] transition"
670
+ >
671
+ {field.options?.map((opt) => (
672
+ <option key={opt} value={opt}>{opt}</option>
673
+ ))}
674
+ </select>
675
+ ) : field.type === "boolean" ? (
676
+ <div className="flex items-center gap-2 pt-1">
677
+ <input
678
+ type="checkbox"
679
+ id={`field-${field.name}`}
680
+ checked={Boolean(dbForm[field.name] ?? field.default ?? false)}
681
+ onChange={(e) => setDbForm((f) => ({ ...f, [field.name]: e.target.checked }))}
682
+ className="w-4 h-4 accent-[#FF8F00]"
683
+ />
684
+ <label htmlFor={`field-${field.name}`} className="text-xs text-slate-500">
685
+ {field.description}
686
+ </label>
687
+ </div>
688
+ ) : field.name === "service_account_json" ? (
689
+ <textarea
690
+ rows={4}
691
+ placeholder={field.description}
692
+ value={String(dbForm[field.name] ?? "")}
693
+ onChange={(e) => setDbForm((f) => ({ ...f, [field.name]: e.target.value }))}
694
+ className="w-full border border-slate-200 rounded-lg px-3 py-2 text-xs bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] placeholder:text-slate-300 transition font-mono"
695
+ />
696
+ ) : (
697
+ <input
698
+ type={field.sensitive ? "password" : field.type === "integer" ? "number" : "text"}
699
+ placeholder={field.default != null ? String(field.default) : field.description}
700
+ value={String(dbForm[field.name] ?? "")}
701
+ onChange={(e) =>
702
+ setDbForm((f) => ({
703
+ ...f,
704
+ [field.name]: field.type === "integer"
705
+ ? (parseInt(e.target.value, 10) || 0)
706
+ : e.target.value,
707
+ }))
708
+ }
709
+ className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] placeholder:text-slate-300 transition"
710
+ />
711
+ )}
712
+ </div>
713
+ ))}
714
+ </div>
715
+
716
+ <button
717
+ onClick={handleDbConnect}
718
+ disabled={
719
+ connecting ||
720
+ !connectionName.trim() ||
721
+ (selectedDbInfo?.fields.filter((f) => f.required).some((f) => !dbForm[f.name]) ?? false)
722
+ }
723
+ className="w-full flex items-center justify-center gap-2 bg-[#FF8F00] hover:bg-[#FF6F00] active:bg-[#E65100] text-white py-2.5 rounded-xl text-sm font-medium transition disabled:opacity-40 disabled:cursor-not-allowed"
724
+ >
725
+ {connecting ? (
726
+ <><Loader2 className="w-4 h-4 animate-spin" /> Connecting…</>
727
+ ) : (
728
+ "Connect"
729
+ )}
730
+ </button>
731
+ </div>
732
+ )}
733
+ </div>
734
+
735
+ {/* Footer */}
736
+ <div className="px-5 py-3 border-t border-slate-100">
737
+ <p className="text-[10px] text-slate-300 text-center">
738
+ {view === "main"
739
+ ? `Supported formats: ${supportedFormatsText}`
740
+ : view === "db-select"
741
+ ? "More integrations coming soon"
742
+ : "Credentials are encrypted at rest"}
743
+ </p>
744
+ </div>
745
+
746
+ </div>
747
+ </div>
748
+ );
749
+ }
src/app/components/Login.tsx ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { useNavigate } from "react-router";
3
+ import { LogIn, Loader2 } from "lucide-react";
4
+ import { login } from "../../services/api";
5
+ import logoUrl from "../../assets/logo.png";
6
+
7
+ export default function Login() {
8
+ const [email, setEmail] = useState("");
9
+ const [password, setPassword] = useState("");
10
+ const [error, setError] = useState("");
11
+ const [isLoading, setIsLoading] = useState(false);
12
+ const navigate = useNavigate();
13
+
14
+ const handleLogin = async (e: React.FormEvent) => {
15
+ e.preventDefault();
16
+ setError("");
17
+
18
+ if (!email || !password) {
19
+ setError("Please enter both email and password");
20
+ return;
21
+ }
22
+
23
+ if (!/\S+@\S+\.\S+/.test(email)) {
24
+ setError("Please enter a valid email address");
25
+ return;
26
+ }
27
+
28
+ setIsLoading(true);
29
+ try {
30
+ const res = await login(email, password);
31
+ const user = {
32
+ user_id: res.data.id,
33
+ email: res.data.email,
34
+ name: res.data.fullname,
35
+ loginTime: new Date().toISOString(),
36
+ };
37
+ localStorage.setItem("chatbot_user", JSON.stringify(user));
38
+ navigate("/");
39
+ } catch (err: unknown) {
40
+ setError(err instanceof Error ? err.message : "Login failed");
41
+ } finally {
42
+ setIsLoading(false);
43
+ }
44
+ };
45
+
46
+ return (
47
+ <div className="relative min-h-screen flex items-center justify-center overflow-hidden bg-[#1e2d45]">
48
+
49
+ {/* ── Dot grid texture ── */}
50
+ <div
51
+ className="absolute inset-0 z-0 opacity-20"
52
+ style={{
53
+ backgroundImage: "radial-gradient(circle, #334155 1px, transparent 1px)",
54
+ backgroundSize: "28px 28px",
55
+ }}
56
+ />
57
+
58
+ {/* ── Gradient orbs — glowing on dark bg ── */}
59
+ {/* Cyan — top left */}
60
+ <div
61
+ className="absolute -top-32 -left-32 w-[520px] h-[520px] rounded-full blur-3xl opacity-[0.18] animate-pulse"
62
+ style={{ background: "#0ea5e9", animationDuration: "5s" }}
63
+ />
64
+ {/* Orange — bottom right */}
65
+ <div
66
+ className="absolute -bottom-40 -right-40 w-[560px] h-[560px] rounded-full blur-3xl opacity-[0.16] animate-pulse"
67
+ style={{ background: "#f97316", animationDuration: "7s" }}
68
+ />
69
+ {/* Green — top right */}
70
+ <div
71
+ className="absolute -top-24 right-1/4 w-[320px] h-[320px] rounded-full blur-3xl opacity-[0.12] animate-pulse"
72
+ style={{ background: "#10b981", animationDuration: "9s" }}
73
+ />
74
+ {/* Purple accent — center left */}
75
+ <div
76
+ className="absolute top-1/2 -left-40 w-[360px] h-[360px] rounded-full blur-3xl opacity-[0.10] animate-pulse"
77
+ style={{ background: "#8b5cf6", animationDuration: "11s" }}
78
+ />
79
+
80
+ {/* ── Neural network — bottom left ── */}
81
+ <svg
82
+ className="absolute bottom-8 left-8 opacity-[0.35] animate-float-slow"
83
+ width="200" height="200" viewBox="0 0 200 200"
84
+ fill="none" xmlns="http://www.w3.org/2000/svg"
85
+ >
86
+ <line x1="20" y1="150" x2="65" y2="90" stroke="#0ea5e9" strokeWidth="1"/>
87
+ <line x1="20" y1="150" x2="55" y2="170" stroke="#0ea5e9" strokeWidth="1"/>
88
+ <line x1="65" y1="90" x2="110" y2="125" stroke="#0ea5e9" strokeWidth="1"/>
89
+ <line x1="65" y1="90" x2="130" y2="55" stroke="#10b981" strokeWidth="1"/>
90
+ <line x1="110" y1="125" x2="170" y2="105" stroke="#0ea5e9" strokeWidth="1"/>
91
+ <line x1="130" y1="55" x2="170" y2="105" stroke="#10b981" strokeWidth="1"/>
92
+ <line x1="130" y1="55" x2="175" y2="30" stroke="#10b981" strokeWidth="1"/>
93
+ <line x1="55" y1="170" x2="110" y2="125" stroke="#0ea5e9" strokeWidth="1"/>
94
+ <line x1="175" y1="30" x2="170" y2="105" stroke="#10b981" strokeWidth="1"/>
95
+ <circle cx="20" cy="150" r="3.5" fill="#0ea5e9" fillOpacity="0.7"/>
96
+ <circle cx="55" cy="170" r="2.5" fill="#0ea5e9" fillOpacity="0.5"/>
97
+ <circle cx="65" cy="90" r="5" fill="#0ea5e9" fillOpacity="0.9"/>
98
+ <circle cx="110" cy="125" r="3.5" fill="#0ea5e9" fillOpacity="0.7"/>
99
+ <circle cx="130" cy="55" r="5" fill="#10b981" fillOpacity="0.9"/>
100
+ <circle cx="170" cy="105" r="3.5" fill="#10b981" fillOpacity="0.7"/>
101
+ <circle cx="175" cy="30" r="2.5" fill="#10b981" fillOpacity="0.5"/>
102
+ </svg>
103
+
104
+ {/* ── Neural network — top right ── */}
105
+ <svg
106
+ className="absolute top-8 right-8 opacity-[0.30] animate-float"
107
+ style={{ "--float-rotate": "0deg" } as React.CSSProperties}
108
+ width="180" height="180" viewBox="0 0 180 180"
109
+ fill="none" xmlns="http://www.w3.org/2000/svg"
110
+ >
111
+ <line x1="160" y1="30" x2="115" y2="80" stroke="#f97316" strokeWidth="1"/>
112
+ <line x1="115" y1="80" x2="70" y2="55" stroke="#f97316" strokeWidth="1"/>
113
+ <line x1="115" y1="80" x2="130" y2="135" stroke="#0ea5e9" strokeWidth="1"/>
114
+ <line x1="70" y1="55" x2="25" y2="90" stroke="#f97316" strokeWidth="1"/>
115
+ <line x1="25" y1="90" x2="80" y2="155" stroke="#0ea5e9" strokeWidth="1"/>
116
+ <line x1="80" y1="155" x2="130" y2="135" stroke="#0ea5e9" strokeWidth="1"/>
117
+ <line x1="70" y1="55" x2="30" y2="25" stroke="#f97316" strokeWidth="1"/>
118
+ <circle cx="160" cy="30" r="3.5" fill="#f97316" fillOpacity="0.7"/>
119
+ <circle cx="115" cy="80" r="5" fill="#f97316" fillOpacity="0.9"/>
120
+ <circle cx="70" cy="55" r="3.5" fill="#f97316" fillOpacity="0.7"/>
121
+ <circle cx="130" cy="135" r="2.5" fill="#0ea5e9" fillOpacity="0.5"/>
122
+ <circle cx="25" cy="90" r="3.5" fill="#0ea5e9" fillOpacity="0.7"/>
123
+ <circle cx="80" cy="155" r="2.5" fill="#0ea5e9" fillOpacity="0.5"/>
124
+ <circle cx="30" cy="25" r="2.5" fill="#f97316" fillOpacity="0.5"/>
125
+ </svg>
126
+
127
+ {/* ── Large ring — top right ── */}
128
+ <div className="absolute -top-24 -right-24 w-80 h-80 rounded-full border border-slate-700 animate-float-slow opacity-50" />
129
+ <div className="absolute -top-10 -right-10 w-48 h-48 rounded-full border border-slate-800 opacity-60" />
130
+
131
+ {/* ── Rotated square — bottom left ── */}
132
+ <div
133
+ className="absolute bottom-16 left-16 w-24 h-24 border border-slate-200 opacity-50 animate-float"
134
+ style={{ transform: "rotate(20deg)", "--float-rotate": "20deg" } as React.CSSProperties}
135
+ />
136
+ <div
137
+ className="absolute bottom-28 left-28 w-12 h-12 border border-slate-200 opacity-40"
138
+ style={{ transform: "rotate(45deg)" }}
139
+ />
140
+
141
+ {/* ── Hexagon — bottom right ── */}
142
+ <svg
143
+ className="absolute bottom-12 right-16 opacity-[0.35] animate-float"
144
+ style={{ "--float-rotate": "0deg", animationDelay: "2s" } as React.CSSProperties}
145
+ width="80" height="80" viewBox="0 0 160 160"
146
+ fill="none" xmlns="http://www.w3.org/2000/svg"
147
+ >
148
+ <path
149
+ d="M 140 80 L 110 132 L 50 132 L 20 80 L 50 28 L 110 28 Z"
150
+ stroke="#f97316" strokeWidth="1.5" fill="none"
151
+ />
152
+ <path
153
+ d="M 122 80 L 101 114 L 59 114 L 38 80 L 59 46 L 101 46 Z"
154
+ stroke="#f97316" strokeWidth="1" strokeOpacity="0.5" fill="none"
155
+ />
156
+ </svg>
157
+
158
+ {/* ── Rotated square — bottom left ── */}
159
+ <div
160
+ className="absolute bottom-16 left-16 w-24 h-24 border border-slate-700 opacity-50 animate-float"
161
+ style={{ transform: "rotate(20deg)", "--float-rotate": "20deg" } as React.CSSProperties}
162
+ />
163
+ <div
164
+ className="absolute bottom-28 left-28 w-12 h-12 border border-slate-700 opacity-30"
165
+ style={{ transform: "rotate(45deg)" }}
166
+ />
167
+
168
+ {/* ── Small floating dots ── */}
169
+ <div className="absolute top-1/4 left-12 w-2 h-2 rounded-full bg-sky-400 opacity-60 animate-float" style={{ animationDelay: "1s" }} />
170
+ <div className="absolute top-1/3 right-20 w-1.5 h-1.5 rounded-full bg-orange-400 opacity-60 animate-float-slow" style={{ animationDelay: "3s" }} />
171
+ <div className="absolute bottom-1/3 left-1/4 w-1.5 h-1.5 rounded-full bg-emerald-400 opacity-60 animate-float" style={{ animationDelay: "0.5s" }} />
172
+ <div className="absolute top-2/3 right-1/3 w-1 h-1 rounded-full bg-violet-400 opacity-50 animate-float-slow" style={{ animationDelay: "4s" }} />
173
+
174
+ {/* ── Login card ── */}
175
+ <div className="relative z-10 w-full max-w-md px-4">
176
+ <div className="bg-white rounded-2xl shadow-2xl shadow-black/40 p-7 border border-white/10">
177
+
178
+ {/* Brand logo */}
179
+ <div className="flex flex-col items-center gap-3 mb-6">
180
+ <img src={logoUrl} alt="DataEyond" className="w-12 h-12 object-contain" />
181
+ <div className="text-center">
182
+ <h1 className="text-xl font-semibold text-slate-900">Welcome Back</h1>
183
+ <p className="text-slate-400 text-sm mt-0.5">Sign in to continue to your chatbot</p>
184
+ </div>
185
+ </div>
186
+
187
+ <form onSubmit={handleLogin} className="space-y-4">
188
+ <div>
189
+ <label htmlFor="email" className="block text-xs font-medium mb-1.5 text-slate-600">
190
+ Email Address
191
+ </label>
192
+ <input
193
+ id="email"
194
+ type="email"
195
+ value={email}
196
+ onChange={(e) => setEmail(e.target.value)}
197
+ className="w-full px-3 py-2 text-sm rounded-xl border border-slate-200 bg-slate-50 focus:outline-none focus:ring-2 focus:ring-sky-400/30 focus:border-sky-400 placeholder:text-slate-300 transition"
198
+ placeholder="you@example.com"
199
+ disabled={isLoading}
200
+ />
201
+ </div>
202
+
203
+ <div>
204
+ <label htmlFor="password" className="block text-xs font-medium mb-1.5 text-slate-600">
205
+ Password
206
+ </label>
207
+ <input
208
+ id="password"
209
+ type="password"
210
+ value={password}
211
+ onChange={(e) => setPassword(e.target.value)}
212
+ className="w-full px-3 py-2 text-sm rounded-xl border border-slate-200 bg-slate-50 focus:outline-none focus:ring-2 focus:ring-sky-400/30 focus:border-sky-400 placeholder:text-slate-300 transition"
213
+ placeholder="Enter your password"
214
+ disabled={isLoading}
215
+ />
216
+ </div>
217
+
218
+ {error && (
219
+ <div className="bg-red-50 border border-red-100 text-red-600 px-3 py-2 rounded-xl text-xs">
220
+ {error}
221
+ </div>
222
+ )}
223
+
224
+ <button
225
+ type="submit"
226
+ disabled={isLoading}
227
+ className="w-full flex items-center justify-center gap-2 bg-[#059669] hover:bg-[#047857] active:bg-[#065F46] text-white py-2.5 text-sm rounded-xl transition font-medium disabled:opacity-50 disabled:cursor-not-allowed mt-1"
228
+ >
229
+ {isLoading ? (
230
+ <Loader2 className="w-4 h-4 animate-spin" />
231
+ ) : (
232
+ <LogIn className="w-4 h-4" />
233
+ )}
234
+ {isLoading ? "Signing in…" : "Sign In"}
235
+ </button>
236
+ </form>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ );
241
+ }
src/app/components/Main.tsx ADDED
@@ -0,0 +1,847 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { useNavigate } from "react-router";
3
+ import {
4
+ Send,
5
+ Plus,
6
+ Trash2,
7
+ LogOut,
8
+ Menu,
9
+ X,
10
+ MessageSquare,
11
+ User,
12
+ Bot,
13
+ Loader2,
14
+ Database,
15
+ } from "lucide-react";
16
+ import ReactMarkdown from "react-markdown";
17
+ import remarkGfm from "remark-gfm";
18
+ import remarkMath from "remark-math";
19
+ import rehypeKatex from "rehype-katex";
20
+ import type { Components } from "react-markdown";
21
+ import KnowledgeManagement from "./KnowledgeManagement";
22
+ import {
23
+ getRooms,
24
+ getRoom,
25
+ createRoom,
26
+ deleteRoom,
27
+ streamChat,
28
+ type ChatSource,
29
+ } from "../../services/api";
30
+
31
+ interface StoredUser {
32
+ user_id: string;
33
+ email: string;
34
+ name: string;
35
+ loginTime: string;
36
+ }
37
+
38
+ interface Message {
39
+ id: string;
40
+ role: "user" | "assistant";
41
+ content: string;
42
+ timestamp: number;
43
+ sources?: ChatSource[];
44
+ }
45
+
46
+ interface ChatRoom {
47
+ id: string;
48
+ title: string;
49
+ messages: Message[];
50
+ createdAt: string;
51
+ updatedAt: string | null;
52
+ messagesLoaded: boolean;
53
+ }
54
+
55
+ // Preprocess markdown to ensure tables are properly separated from surrounding text
56
+ function preprocessMarkdown(content: string): string {
57
+ // Step 1: Split concatenated table rows — "| val ||" means end of row + start of next
58
+ let result = content.replace(/\|\|/g, "|\n|");
59
+
60
+ // Step 2: Per-line — if a line has non-pipe text before a pipe table row, split them
61
+ const lines = result.split("\n");
62
+ const processed = lines.map((line) => {
63
+ const tableStart = line.indexOf("|");
64
+ if (tableStart > 0) {
65
+ const tableContent = line.slice(tableStart);
66
+ // Confirm it looks like a table row (at least 2 pipes)
67
+ if ((tableContent.match(/\|/g) ?? []).length >= 2) {
68
+ return line.slice(0, tableStart).trimEnd() + "\n\n" + tableContent;
69
+ }
70
+ }
71
+ return line;
72
+ });
73
+ result = processed.join("\n");
74
+
75
+ // Step 3: Ensure blank line before table rows preceded by non-table text (not another row ending with |)
76
+ result = result.replace(/([^|\n])\n(\|)/g, "$1\n\n$2");
77
+
78
+ return result;
79
+ }
80
+
81
+ // Markdown component overrides for clean rendering inside chat bubbles
82
+ const markdownComponents: Components = {
83
+ p: ({ children }) => (
84
+ <p className="text-sm mb-2 last:mb-0 leading-relaxed">{children}</p>
85
+ ),
86
+ h1: ({ children }) => (
87
+ <h1 className="text-lg font-bold mb-3 mt-4 first:mt-0">{children}</h1>
88
+ ),
89
+ h2: ({ children }) => (
90
+ <h2 className="text-base font-bold mb-2 mt-3 first:mt-0">{children}</h2>
91
+ ),
92
+ h3: ({ children }) => (
93
+ <h3 className="text-sm font-semibold mb-2 mt-2 first:mt-0">{children}</h3>
94
+ ),
95
+ ul: ({ children }) => (
96
+ <ul className="list-disc pl-5 mb-2 space-y-1 text-sm">{children}</ul>
97
+ ),
98
+ ol: ({ children }) => (
99
+ <ol className="list-decimal pl-5 mb-2 space-y-1 text-sm">{children}</ol>
100
+ ),
101
+ li: ({ children }) => <li className="text-sm leading-relaxed">{children}</li>,
102
+ code: ({ children, className }) => {
103
+ const isBlock = className?.startsWith("language-");
104
+ if (isBlock) {
105
+ return (
106
+ <code className="block text-xs font-mono text-slate-100 leading-relaxed">
107
+ {children}
108
+ </code>
109
+ );
110
+ }
111
+ return (
112
+ <code className="bg-slate-100 text-pink-600 px-1.5 py-0.5 rounded text-xs font-mono">
113
+ {children}
114
+ </code>
115
+ );
116
+ },
117
+ pre: ({ children }) => (
118
+ <pre className="bg-slate-900 rounded-lg p-3 mb-2 mt-1 overflow-x-auto text-xs">
119
+ {children}
120
+ </pre>
121
+ ),
122
+ blockquote: ({ children }) => (
123
+ <blockquote className="border-l-4 border-slate-300 pl-3 text-slate-500 italic mb-2 text-sm">
124
+ {children}
125
+ </blockquote>
126
+ ),
127
+ table: ({ children }) => (
128
+ <div className="overflow-x-auto mb-2">
129
+ <table className="w-full text-sm border-collapse">{children}</table>
130
+ </div>
131
+ ),
132
+ th: ({ children }) => (
133
+ <th className="border border-slate-200 px-3 py-1.5 bg-slate-100 font-medium text-left text-xs">
134
+ {children}
135
+ </th>
136
+ ),
137
+ td: ({ children }) => (
138
+ <td className="border border-slate-200 px-3 py-1.5 text-xs">{children}</td>
139
+ ),
140
+ a: ({ children, href }) => (
141
+ <a
142
+ href={href}
143
+ target="_blank"
144
+ rel="noopener noreferrer"
145
+ className="text-blue-600 underline hover:text-blue-800"
146
+ >
147
+ {children}
148
+ </a>
149
+ ),
150
+ strong: ({ children }) => (
151
+ <strong className="font-semibold">{children}</strong>
152
+ ),
153
+ hr: () => <hr className="border-slate-200 my-3" />,
154
+ };
155
+
156
+ // Typing indicator — three bouncing dots
157
+ function useLoadingMessages() {
158
+ const [messages, setMessages] = useState<string[]>([]);
159
+
160
+ useEffect(() => {
161
+ fetch("/loading-messages.yaml")
162
+ .then((r) => r.text())
163
+ .then((text) => {
164
+ const parsed = text
165
+ .split("\n")
166
+ .filter((l) => l.trimStart().startsWith("- "))
167
+ .map((l) => l.replace(/^\s*- /, "").trim())
168
+ .filter(Boolean);
169
+ if (parsed.length > 0) setMessages(parsed);
170
+ })
171
+ .catch(() => {});
172
+ }, []);
173
+
174
+ return messages;
175
+ }
176
+
177
+ function TypingIndicator() {
178
+ const messages = useLoadingMessages();
179
+ const [index, setIndex] = useState(0);
180
+
181
+ useEffect(() => {
182
+ if (messages.length === 0) return;
183
+ setIndex(Math.floor(Math.random() * messages.length));
184
+ const id = setInterval(() => {
185
+ setIndex((prev) => {
186
+ let next: number;
187
+ do { next = Math.floor(Math.random() * messages.length); } while (messages.length > 1 && next === prev);
188
+ return next;
189
+ });
190
+ }, 300);
191
+ return () => clearInterval(id);
192
+ }, [messages]);
193
+
194
+ if (messages.length === 0) {
195
+ return (
196
+ <div className="flex gap-1.5 items-center py-1 px-0.5">
197
+ <span className="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: "0ms", animationDuration: "1s" }} />
198
+ <span className="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: "200ms", animationDuration: "1s" }} />
199
+ <span className="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: "400ms", animationDuration: "1s" }} />
200
+ </div>
201
+ );
202
+ }
203
+
204
+ return (
205
+ <div className="flex items-center gap-2 py-1 px-0.5 text-sm text-slate-400 italic">
206
+ <span className="inline-block w-2 h-2 rounded-full bg-slate-400 animate-pulse" />
207
+ <span>{messages[index]}…</span>
208
+ </div>
209
+ );
210
+ }
211
+
212
+ export default function Main() {
213
+ const navigate = useNavigate();
214
+ const [sidebarOpen, setSidebarOpen] = useState(true);
215
+ const [chats, setChats] = useState<ChatRoom[]>([]);
216
+ const [currentChatId, setCurrentChatId] = useState<string | null>(null);
217
+ const [input, setInput] = useState("");
218
+ const [isStreaming, setIsStreaming] = useState(false);
219
+ const [streamingMsgId, setStreamingMsgId] = useState<string | null>(null);
220
+ const [roomsLoading, setRoomsLoading] = useState(false);
221
+ const [roomsError, setRoomsError] = useState<string | null>(null);
222
+ const messagesEndRef = useRef<HTMLDivElement>(null);
223
+ const [user, setUser] = useState<StoredUser | null>(null);
224
+ const [knowledgeOpen, setKnowledgeOpen] = useState(false);
225
+ const abortControllerRef = useRef<AbortController | null>(null);
226
+
227
+ useEffect(() => {
228
+ const storedUser = localStorage.getItem("chatbot_user");
229
+ if (storedUser) {
230
+ const parsedUser: StoredUser = JSON.parse(storedUser);
231
+ setUser(parsedUser);
232
+ loadRooms(parsedUser.user_id);
233
+ }
234
+ }, []);
235
+
236
+ useEffect(() => {
237
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
238
+ }, [currentChatId, chats]);
239
+
240
+ useEffect(() => {
241
+ if (!currentChatId) return;
242
+ const chat = chats.find((c) => c.id === currentChatId);
243
+ if (chat && !chat.messagesLoaded) {
244
+ loadRoomMessages(currentChatId);
245
+ }
246
+ // eslint-disable-next-line react-hooks/exhaustive-deps
247
+ }, [currentChatId]);
248
+
249
+ const loadRooms = async (userId: string) => {
250
+ setRoomsLoading(true);
251
+ setRoomsError(null);
252
+ try {
253
+ const apiRooms = await getRooms(userId);
254
+ const mapped: ChatRoom[] = apiRooms.map((r) => ({
255
+ id: r.id,
256
+ title: r.title,
257
+ messages: [],
258
+ createdAt: r.created_at,
259
+ updatedAt: r.updated_at,
260
+ messagesLoaded: false,
261
+ }));
262
+ setChats(mapped);
263
+ if (mapped.length > 0) {
264
+ setCurrentChatId(mapped[0].id);
265
+ }
266
+ } catch (err) {
267
+ setRoomsError(
268
+ err instanceof Error ? err.message : "Failed to load chats"
269
+ );
270
+ } finally {
271
+ setRoomsLoading(false);
272
+ }
273
+ };
274
+
275
+ const loadRoomMessages = async (roomId: string) => {
276
+ try {
277
+ const detail = await getRoom(roomId);
278
+ const messages: Message[] = detail.messages.map((m) => ({
279
+ id: m.id,
280
+ role: m.role,
281
+ content: m.content,
282
+ timestamp: new Date(m.created_at).getTime(),
283
+ sources: m.sources ?? [],
284
+ }));
285
+ setChats((prev) =>
286
+ prev.map((chat) =>
287
+ chat.id === roomId
288
+ ? { ...chat, messages, messagesLoaded: true }
289
+ : chat
290
+ )
291
+ );
292
+ } catch {
293
+ setChats((prev) =>
294
+ prev.map((chat) =>
295
+ chat.id === roomId ? { ...chat, messagesLoaded: true } : chat
296
+ )
297
+ );
298
+ }
299
+ };
300
+
301
+ const currentChat = chats.find((chat) => chat.id === currentChatId);
302
+
303
+ const createNewChat = () => {
304
+ setCurrentChatId(null);
305
+ };
306
+
307
+ const deleteChat = async (chatId: string) => {
308
+ if (!user) return;
309
+ try {
310
+ await deleteRoom(chatId, user.user_id);
311
+ } catch {
312
+ return;
313
+ }
314
+ const updatedChats = chats.filter((chat) => chat.id !== chatId);
315
+ setChats(updatedChats);
316
+ if (currentChatId === chatId) {
317
+ setCurrentChatId(updatedChats.length > 0 ? updatedChats[0].id : null);
318
+ }
319
+ };
320
+
321
+ const deleteAllChats = async () => {
322
+ if (!user) return;
323
+ await Promise.allSettled(
324
+ chats.map((chat) => deleteRoom(chat.id, user.user_id))
325
+ );
326
+ setChats([]);
327
+ setCurrentChatId(null);
328
+ };
329
+
330
+ const handleLogout = () => {
331
+ localStorage.removeItem("chatbot_user");
332
+ navigate("/login");
333
+ };
334
+
335
+ const handleSend = async () => {
336
+ if (!input.trim() || isStreaming || !user) return;
337
+
338
+ let roomId = currentChatId;
339
+
340
+ if (!roomId) {
341
+ try {
342
+ const res = await createRoom(user.user_id, input.slice(0, 50));
343
+ const newRoom: ChatRoom = {
344
+ id: res.data.id,
345
+ title: res.data.title,
346
+ messages: [],
347
+ createdAt: res.data.created_at,
348
+ updatedAt: res.data.updated_at,
349
+ messagesLoaded: true,
350
+ };
351
+ setChats((prev) => [newRoom, ...prev]);
352
+ roomId = newRoom.id;
353
+ setCurrentChatId(roomId);
354
+ } catch {
355
+ return;
356
+ }
357
+ }
358
+
359
+ const userMessage: Message = {
360
+ id: crypto.randomUUID(),
361
+ role: "user",
362
+ content: input,
363
+ timestamp: Date.now(),
364
+ };
365
+
366
+ setChats((prev) =>
367
+ prev.map((chat) =>
368
+ chat.id === roomId
369
+ ? {
370
+ ...chat,
371
+ messages: [...chat.messages, userMessage],
372
+ updatedAt: new Date().toISOString(),
373
+ }
374
+ : chat
375
+ )
376
+ );
377
+
378
+ const sentMessage = input;
379
+ setInput("");
380
+ setIsStreaming(true);
381
+
382
+ const assistantMsgId = crypto.randomUUID();
383
+ setStreamingMsgId(assistantMsgId);
384
+
385
+ setChats((prev) =>
386
+ prev.map((chat) =>
387
+ chat.id === roomId
388
+ ? {
389
+ ...chat,
390
+ messages: [
391
+ ...chat.messages,
392
+ {
393
+ id: assistantMsgId,
394
+ role: "assistant",
395
+ content: "",
396
+ timestamp: Date.now(),
397
+ sources: [],
398
+ },
399
+ ],
400
+ }
401
+ : chat
402
+ )
403
+ );
404
+
405
+ abortControllerRef.current = new AbortController();
406
+
407
+ try {
408
+ const response = await streamChat(user.user_id, roomId, sentMessage);
409
+
410
+ if (!response.body) throw new Error("No response body");
411
+
412
+ const reader = response.body.getReader();
413
+ const decoder = new TextDecoder();
414
+ let buffer = "";
415
+ let currentEvent = "";
416
+
417
+ while (true) {
418
+ const { done, value } = await reader.read();
419
+ if (done) break;
420
+
421
+ buffer += decoder.decode(value, { stream: true });
422
+ const lines = buffer.split(/\r?\n/);
423
+ buffer = lines.pop() ?? "";
424
+
425
+ for (const line of lines) {
426
+ if (line.startsWith("event:")) {
427
+ currentEvent = line.replace("event:", "").trim();
428
+ } else if (line.startsWith("data:")) {
429
+ const data = line.replace(/^data: ?/, "");
430
+
431
+ if (currentEvent === "sources" && data) {
432
+ const sources: ChatSource[] = JSON.parse(data);
433
+ setChats((prev) =>
434
+ prev.map((chat) =>
435
+ chat.id === roomId
436
+ ? {
437
+ ...chat,
438
+ messages: chat.messages.map((m) =>
439
+ m.id === assistantMsgId ? { ...m, sources } : m
440
+ ),
441
+ }
442
+ : chat
443
+ )
444
+ );
445
+ } else if (currentEvent === "chunk" && data) {
446
+ setChats((prev) =>
447
+ prev.map((chat) =>
448
+ chat.id === roomId
449
+ ? {
450
+ ...chat,
451
+ messages: chat.messages.map((m) =>
452
+ m.id === assistantMsgId
453
+ ? { ...m, content: m.content + data }
454
+ : m
455
+ ),
456
+ }
457
+ : chat
458
+ )
459
+ );
460
+ } else if (currentEvent === "message" && data) {
461
+ setChats((prev) =>
462
+ prev.map((chat) =>
463
+ chat.id === roomId
464
+ ? {
465
+ ...chat,
466
+ messages: chat.messages.map((m) =>
467
+ m.id === assistantMsgId
468
+ ? { ...m, content: data }
469
+ : m
470
+ ),
471
+ }
472
+ : chat
473
+ )
474
+ );
475
+ } else if (currentEvent === "done") {
476
+ break;
477
+ }
478
+ }
479
+ }
480
+ }
481
+ // Re-fetch from API after stream ends to get the authoritative
482
+ // stored content — guarantees formatting matches page-refresh behavior
483
+ await loadRoomMessages(roomId);
484
+ } catch (err: unknown) {
485
+ if ((err as Error).name !== "AbortError") {
486
+ setChats((prev) =>
487
+ prev.map((chat) =>
488
+ chat.id === roomId
489
+ ? {
490
+ ...chat,
491
+ messages: chat.messages.map((m) =>
492
+ m.id === assistantMsgId
493
+ ? {
494
+ ...m,
495
+ content:
496
+ "Sorry, I couldn't get a response. Please try again.",
497
+ }
498
+ : m
499
+ ),
500
+ }
501
+ : chat
502
+ )
503
+ );
504
+ }
505
+ } finally {
506
+ setIsStreaming(false);
507
+ setStreamingMsgId(null);
508
+ abortControllerRef.current = null;
509
+ }
510
+ };
511
+
512
+ const handleKeyPress = (e: React.KeyboardEvent) => {
513
+ if (e.key === "Enter" && !e.shiftKey) {
514
+ e.preventDefault();
515
+ handleSend();
516
+ }
517
+ };
518
+
519
+ return (
520
+ <div className="flex h-screen bg-slate-50">
521
+ {/* Sidebar */}
522
+ <div
523
+ className={`${
524
+ sidebarOpen ? "w-64" : "w-0"
525
+ } bg-gradient-to-b from-[#059669] to-[#047857] text-white transition-all duration-300 flex flex-col overflow-hidden`}
526
+ >
527
+ <div className="p-3 border-b border-white/20">
528
+ <button
529
+ onClick={createNewChat}
530
+ className="w-full flex items-center justify-center gap-2 bg-white/20 hover:bg-white/30 px-3 py-2 rounded-lg transition text-sm"
531
+ >
532
+ <Plus className="w-4 h-4" />
533
+ New Chat
534
+ </button>
535
+ </div>
536
+
537
+ <div className="flex-1 overflow-y-auto p-2 space-y-1">
538
+ {roomsLoading ? (
539
+ <div className="flex justify-center py-4">
540
+ <Loader2 className="w-4 h-4 animate-spin text-white/70" />
541
+ </div>
542
+ ) : roomsError ? (
543
+ <p className="text-xs text-red-200 text-center px-2 py-2">
544
+ {roomsError}
545
+ </p>
546
+ ) : (
547
+ chats.map((chat) => (
548
+ <div
549
+ key={chat.id}
550
+ className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition group ${
551
+ currentChatId === chat.id
552
+ ? "bg-white/25"
553
+ : "hover:bg-white/15"
554
+ }`}
555
+ >
556
+ <MessageSquare className="w-3.5 h-3.5 flex-shrink-0" />
557
+ <div
558
+ className="flex-1 truncate text-sm"
559
+ onClick={() => setCurrentChatId(chat.id)}
560
+ >
561
+ {chat.title}
562
+ </div>
563
+ <button
564
+ onClick={(e) => {
565
+ e.stopPropagation();
566
+ deleteChat(chat.id);
567
+ }}
568
+ className="opacity-0 group-hover:opacity-100 transition"
569
+ >
570
+ <Trash2 className="w-3.5 h-3.5 text-red-100 hover:text-white" />
571
+ </button>
572
+ </div>
573
+ ))
574
+ )}
575
+ </div>
576
+
577
+ <div className="border-t border-white/20 p-3 space-y-2">
578
+ {chats.length > 0 && (
579
+ <button
580
+ onClick={deleteAllChats}
581
+ className="w-full flex items-center justify-center gap-2 text-red-100 hover:text-white px-3 py-2 rounded-lg hover:bg-white/15 transition text-xs"
582
+ >
583
+ <Trash2 className="w-3.5 h-3.5" />
584
+ Clear All Chats
585
+ </button>
586
+ )}
587
+
588
+ <div className="flex items-center gap-2 p-2 rounded-lg bg-white/20">
589
+ <div className="w-7 h-7 bg-white/30 rounded-full flex items-center justify-center">
590
+ <User className="w-3.5 h-3.5" />
591
+ </div>
592
+ <div className="flex-1 min-w-0">
593
+ <div className="text-xs truncate">{user?.name}</div>
594
+ <div className="text-[10px] text-white/70 truncate">
595
+ {user?.email}
596
+ </div>
597
+ </div>
598
+ <button
599
+ onClick={handleLogout}
600
+ className="text-white/70 hover:text-white transition"
601
+ title="Logout"
602
+ >
603
+ <LogOut className="w-3.5 h-3.5" />
604
+ </button>
605
+ </div>
606
+ </div>
607
+ </div>
608
+
609
+ {/* Main Content */}
610
+ <div className="flex-1 flex flex-col min-w-0 relative">
611
+
612
+ {/* ── Subtle background — harmonizes with Login page ── */}
613
+ <div className="absolute inset-0 pointer-events-none z-0 overflow-hidden">
614
+ {/* Dot grid */}
615
+ <div
616
+ className="absolute inset-0 opacity-[0.13]"
617
+ style={{
618
+ backgroundImage: "radial-gradient(circle, #94a3b8 1px, transparent 1px)",
619
+ backgroundSize: "28px 28px",
620
+ }}
621
+ />
622
+ {/* Cyan orb — top right */}
623
+ <div
624
+ className="absolute -top-28 -right-28 w-80 h-80 rounded-full blur-3xl opacity-[0.08]"
625
+ style={{ background: "#0ea5e9" }}
626
+ />
627
+ {/* Orange orb — bottom left */}
628
+ <div
629
+ className="absolute -bottom-28 -left-28 w-96 h-96 rounded-full blur-3xl opacity-[0.07]"
630
+ style={{ background: "#f97316" }}
631
+ />
632
+ {/* Green orb — center left */}
633
+ <div
634
+ className="absolute top-1/2 -left-32 w-64 h-64 rounded-full blur-3xl opacity-[0.05]"
635
+ style={{ background: "#10b981" }}
636
+ />
637
+ {/* Neural network — bottom right */}
638
+ <svg
639
+ className="absolute bottom-20 right-4 opacity-[0.07]"
640
+ width="160" height="160" viewBox="0 0 200 200"
641
+ fill="none" xmlns="http://www.w3.org/2000/svg"
642
+ >
643
+ <line x1="20" y1="150" x2="65" y2="90" stroke="#0ea5e9" strokeWidth="1.2"/>
644
+ <line x1="20" y1="150" x2="55" y2="170" stroke="#0ea5e9" strokeWidth="1.2"/>
645
+ <line x1="65" y1="90" x2="110" y2="125" stroke="#0ea5e9" strokeWidth="1.2"/>
646
+ <line x1="65" y1="90" x2="130" y2="55" stroke="#10b981" strokeWidth="1.2"/>
647
+ <line x1="110" y1="125" x2="170" y2="105" stroke="#0ea5e9" strokeWidth="1.2"/>
648
+ <line x1="130" y1="55" x2="170" y2="105" stroke="#10b981" strokeWidth="1.2"/>
649
+ <line x1="130" y1="55" x2="175" y2="30" stroke="#10b981" strokeWidth="1.2"/>
650
+ <line x1="55" y1="170" x2="110" y2="125" stroke="#0ea5e9" strokeWidth="1.2"/>
651
+ <line x1="175" y1="30" x2="170" y2="105" stroke="#10b981" strokeWidth="1.2"/>
652
+ <circle cx="20" cy="150" r="4" fill="#0ea5e9"/>
653
+ <circle cx="55" cy="170" r="3" fill="#0ea5e9"/>
654
+ <circle cx="65" cy="90" r="5.5" fill="#0ea5e9"/>
655
+ <circle cx="110" cy="125" r="4" fill="#0ea5e9"/>
656
+ <circle cx="130" cy="55" r="5.5" fill="#10b981"/>
657
+ <circle cx="170" cy="105" r="4" fill="#10b981"/>
658
+ <circle cx="175" cy="30" r="3" fill="#10b981"/>
659
+ </svg>
660
+ {/* Neural network — top left (small) */}
661
+ <svg
662
+ className="absolute top-16 left-4 opacity-[0.06]"
663
+ width="110" height="110" viewBox="0 0 180 180"
664
+ fill="none" xmlns="http://www.w3.org/2000/svg"
665
+ >
666
+ <line x1="160" y1="30" x2="115" y2="80" stroke="#f97316" strokeWidth="1.2"/>
667
+ <line x1="115" y1="80" x2="70" y2="55" stroke="#f97316" strokeWidth="1.2"/>
668
+ <line x1="115" y1="80" x2="130" y2="135" stroke="#0ea5e9" strokeWidth="1.2"/>
669
+ <line x1="70" y1="55" x2="25" y2="90" stroke="#f97316" strokeWidth="1.2"/>
670
+ <line x1="25" y1="90" x2="80" y2="155" stroke="#0ea5e9" strokeWidth="1.2"/>
671
+ <line x1="80" y1="155" x2="130" y2="135" stroke="#0ea5e9" strokeWidth="1.2"/>
672
+ <circle cx="160" cy="30" r="4" fill="#f97316"/>
673
+ <circle cx="115" cy="80" r="5.5" fill="#f97316"/>
674
+ <circle cx="70" cy="55" r="4" fill="#f97316"/>
675
+ <circle cx="130" cy="135" r="3" fill="#0ea5e9"/>
676
+ <circle cx="25" cy="90" r="4" fill="#0ea5e9"/>
677
+ <circle cx="80" cy="155" r="3" fill="#0ea5e9"/>
678
+ </svg>
679
+ </div>
680
+
681
+ {/* Header */}
682
+ <div className="relative z-10 bg-white border-b border-slate-200 p-3 flex items-center gap-3">
683
+ <button
684
+ onClick={() => setSidebarOpen(!sidebarOpen)}
685
+ className="text-slate-600 hover:text-slate-900 transition"
686
+ >
687
+ {sidebarOpen ? (
688
+ <X className="w-5 h-5" />
689
+ ) : (
690
+ <Menu className="w-5 h-5" />
691
+ )}
692
+ </button>
693
+ <h1 className="text-base text-slate-900 flex-1 truncate">
694
+ {currentChat?.title || "Chatbot"}
695
+ </h1>
696
+ <button
697
+ onClick={() => setKnowledgeOpen(true)}
698
+ className="flex items-center gap-2 bg-[#F59E0B] hover:bg-[#D97706] text-white px-3 py-2 rounded-lg transition-all duration-200 hover:scale-105 text-sm flex-shrink-0"
699
+ >
700
+ <Database className="w-4 h-4" />
701
+ Knowledge
702
+ </button>
703
+ </div>
704
+
705
+ {/* Messages */}
706
+ <div className="relative z-10 flex-1 overflow-y-auto p-4 space-y-4">
707
+ {currentChat?.messages.length === 0 && (
708
+ <div className="flex items-center justify-center h-full">
709
+ <div className="text-center">
710
+ <MessageSquare className="w-12 h-12 text-slate-300 mx-auto mb-3" />
711
+ <h2 className="text-base text-slate-600 mb-1">
712
+ Start a conversation
713
+ </h2>
714
+ <p className="text-sm text-slate-400">
715
+ Send a message to begin chatting
716
+ </p>
717
+ </div>
718
+ </div>
719
+ )}
720
+
721
+ {currentChat?.messages.map((message) => (
722
+ <div
723
+ key={message.id}
724
+ className={`flex ${
725
+ message.role === "user" ? "justify-end" : "justify-start"
726
+ }`}
727
+ >
728
+ {/* Avatar for assistant */}
729
+ {message.role === "assistant" && (
730
+ <div className="w-7 h-7 rounded-full bg-gradient-to-br from-[#059669] to-[#047857] flex items-center justify-center flex-shrink-0 mt-0.5 mr-2">
731
+ <Bot className="w-3.5 h-3.5 text-white" />
732
+ </div>
733
+ )}
734
+
735
+ <div
736
+ className={`max-w-2xl px-4 py-3 rounded-2xl ${
737
+ message.role === "user"
738
+ ? "bg-[#3B82F6] text-white rounded-tr-sm shadow-sm"
739
+ : "bg-[#F3F4F6] border-0 text-slate-900 rounded-tl-sm shadow-sm"
740
+ }`}
741
+ >
742
+ {message.role === "user" ? (
743
+ <p className="text-sm whitespace-pre-wrap break-words leading-relaxed">
744
+ {message.content}
745
+ </p>
746
+ ) : message.content === "" && streamingMsgId === message.id ? (
747
+ // Waiting for first chunk — show typing indicator
748
+ <TypingIndicator />
749
+ ) : (
750
+ // Render markdown for assistant messages
751
+ <div className="text-slate-900">
752
+ <ReactMarkdown
753
+ remarkPlugins={[remarkGfm, remarkMath]}
754
+ rehypePlugins={[rehypeKatex]}
755
+ components={markdownComponents}
756
+ >
757
+ {streamingMsgId === message.id
758
+ ? message.content
759
+ : preprocessMarkdown(message.content)}
760
+ </ReactMarkdown>
761
+ </div>
762
+ )}
763
+
764
+ {/* Sources */}
765
+ {message.role === "assistant" &&
766
+ message.sources &&
767
+ message.sources.length > 0 && (
768
+ <div className="mt-2 pt-2 border-t border-slate-100">
769
+ <p className="text-[10px] text-slate-400 mb-1.5">
770
+ Sources:
771
+ </p>
772
+ <div className="flex flex-wrap gap-1">
773
+ {message.sources.map((src, i) => (
774
+ <span
775
+ key={i}
776
+ className="text-[10px] bg-slate-100 text-slate-600 px-2 py-0.5 rounded-full border border-slate-200"
777
+ title={
778
+ src.page_label
779
+ ? `Page ${src.page_label}`
780
+ : undefined
781
+ }
782
+ >
783
+ 📄 {src.filename}
784
+ {src.page_label ? ` p.${src.page_label}` : ""}
785
+ </span>
786
+ ))}
787
+ </div>
788
+ </div>
789
+ )}
790
+ </div>
791
+ </div>
792
+ ))}
793
+
794
+ {!currentChat && chats.length === 0 && !roomsLoading && (
795
+ <div className="flex items-center justify-center h-full">
796
+ <div className="text-center">
797
+ <MessageSquare className="w-12 h-12 text-slate-300 mx-auto mb-3" />
798
+ <h2 className="text-base text-slate-600 mb-1">
799
+ Welcome to Chatbot
800
+ </h2>
801
+ <p className="text-sm text-slate-400">
802
+ Create a new chat to get started
803
+ </p>
804
+ </div>
805
+ </div>
806
+ )}
807
+
808
+ <div ref={messagesEndRef} />
809
+ </div>
810
+
811
+ {/* Input Area */}
812
+ <div className="relative z-10 bg-white border-t border-slate-200 p-3 shadow-[0_-2px_10px_rgba(0,0,0,0.06)]">
813
+ <div className="max-w-4xl mx-auto">
814
+ <div className="flex gap-2 items-end">
815
+ <textarea
816
+ value={input}
817
+ onChange={(e) => setInput(e.target.value)}
818
+ onKeyDown={handleKeyPress}
819
+ placeholder="Ask me anything... (Enter to send, Shift+Enter for newline)"
820
+ rows={1}
821
+ className="flex-1 px-3 py-2 text-sm rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-[#3B82F6] focus:border-transparent resize-none max-h-32"
822
+ disabled={isStreaming}
823
+ />
824
+ <button
825
+ onClick={handleSend}
826
+ disabled={!input.trim() || isStreaming}
827
+ className="bg-[#3B82F6] hover:bg-[#2563EB] text-white p-2.5 rounded-lg transition-all duration-200 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0"
828
+ >
829
+ {isStreaming ? (
830
+ <Loader2 className="w-4 h-4 animate-spin" />
831
+ ) : (
832
+ <Send className="w-4 h-4" />
833
+ )}
834
+ </button>
835
+ </div>
836
+ </div>
837
+ </div>
838
+ </div>
839
+
840
+ {/* Knowledge Management Modal */}
841
+ <KnowledgeManagement
842
+ open={knowledgeOpen}
843
+ onClose={() => setKnowledgeOpen(false)}
844
+ />
845
+ </div>
846
+ );
847
+ }
src/app/components/figma/ImageWithFallback.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react'
2
+
3
+ const ERROR_IMG_SRC =
4
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
5
+
6
+ export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElement>) {
7
+ const [didError, setDidError] = useState(false)
8
+
9
+ const handleError = () => {
10
+ setDidError(true)
11
+ }
12
+
13
+ const { src, alt, style, className, ...rest } = props
14
+
15
+ return didError ? (
16
+ <div
17
+ className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
18
+ style={style}
19
+ >
20
+ <div className="flex items-center justify-center w-full h-full">
21
+ <img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src} />
22
+ </div>
23
+ </div>
24
+ ) : (
25
+ <img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
26
+ )
27
+ }
src/app/components/ui/accordion.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
5
+ import { ChevronDownIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function Accordion({
10
+ ...props
11
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
13
+ }
14
+
15
+ function AccordionItem({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn("border-b last:border-b-0", className)}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ "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",
39
+ className,
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ );
48
+ }
49
+
50
+ function AccordionContent({
51
+ className,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55
+ return (
56
+ <AccordionPrimitive.Content
57
+ data-slot="accordion-content"
58
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
59
+ {...props}
60
+ >
61
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
62
+ </AccordionPrimitive.Content>
63
+ );
64
+ }
65
+
66
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
src/app/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
5
+
6
+ import { cn } from "./utils";
7
+ import { buttonVariants } from "./button";
8
+
9
+ function AlertDialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
12
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
13
+ }
14
+
15
+ function AlertDialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
18
+ return (
19
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
20
+ );
21
+ }
22
+
23
+ function AlertDialogPortal({
24
+ ...props
25
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
26
+ return (
27
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
28
+ );
29
+ }
30
+
31
+ function AlertDialogOverlay({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
35
+ return (
36
+ <AlertDialogPrimitive.Overlay
37
+ data-slot="alert-dialog-overlay"
38
+ className={cn(
39
+ "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",
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ function AlertDialogContent({
48
+ className,
49
+ ...props
50
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
51
+ return (
52
+ <AlertDialogPortal>
53
+ <AlertDialogOverlay />
54
+ <AlertDialogPrimitive.Content
55
+ data-slot="alert-dialog-content"
56
+ className={cn(
57
+ "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",
58
+ className,
59
+ )}
60
+ {...props}
61
+ />
62
+ </AlertDialogPortal>
63
+ );
64
+ }
65
+
66
+ function AlertDialogHeader({
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"div">) {
70
+ return (
71
+ <div
72
+ data-slot="alert-dialog-header"
73
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ function AlertDialogFooter({
80
+ className,
81
+ ...props
82
+ }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="alert-dialog-footer"
86
+ className={cn(
87
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
88
+ className,
89
+ )}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function AlertDialogTitle({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
99
+ return (
100
+ <AlertDialogPrimitive.Title
101
+ data-slot="alert-dialog-title"
102
+ className={cn("text-lg font-semibold", className)}
103
+ {...props}
104
+ />
105
+ );
106
+ }
107
+
108
+ function AlertDialogDescription({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
112
+ return (
113
+ <AlertDialogPrimitive.Description
114
+ data-slot="alert-dialog-description"
115
+ className={cn("text-muted-foreground text-sm", className)}
116
+ {...props}
117
+ />
118
+ );
119
+ }
120
+
121
+ function AlertDialogAction({
122
+ className,
123
+ ...props
124
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
125
+ return (
126
+ <AlertDialogPrimitive.Action
127
+ className={cn(buttonVariants(), className)}
128
+ {...props}
129
+ />
130
+ );
131
+ }
132
+
133
+ function AlertDialogCancel({
134
+ className,
135
+ ...props
136
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
137
+ return (
138
+ <AlertDialogPrimitive.Cancel
139
+ className={cn(buttonVariants({ variant: "outline" }), className)}
140
+ {...props}
141
+ />
142
+ );
143
+ }
144
+
145
+ export {
146
+ AlertDialog,
147
+ AlertDialogPortal,
148
+ AlertDialogOverlay,
149
+ AlertDialogTrigger,
150
+ AlertDialogContent,
151
+ AlertDialogHeader,
152
+ AlertDialogFooter,
153
+ AlertDialogTitle,
154
+ AlertDialogDescription,
155
+ AlertDialogAction,
156
+ AlertDialogCancel,
157
+ };
src/app/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 "./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 };
src/app/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
4
+
5
+ function AspectRatio({
6
+ ...props
7
+ }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
8
+ return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
9
+ }
10
+
11
+ export { AspectRatio };
src/app/components/ui/avatar.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function Avatar({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ data-slot="avatar"
15
+ className={cn(
16
+ "relative flex size-10 shrink-0 overflow-hidden rounded-full",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ function AvatarImage({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28
+ return (
29
+ <AvatarPrimitive.Image
30
+ data-slot="avatar-image"
31
+ className={cn("aspect-square size-full", className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function AvatarFallback({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41
+ return (
42
+ <AvatarPrimitive.Fallback
43
+ data-slot="avatar-fallback"
44
+ className={cn(
45
+ "bg-muted flex size-full items-center justify-center rounded-full",
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ export { Avatar, AvatarImage, AvatarFallback };
src/app/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 "./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 };
src/app/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 "./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
+ };
src/app/components/ui/button.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 "./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-background text-foreground hover:bg-accent hover:text-accent-foreground dark:bg-input/30 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 hover:text-accent-foreground 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 rounded-md",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ },
35
+ );
36
+
37
+ function Button({
38
+ className,
39
+ variant,
40
+ size,
41
+ asChild = false,
42
+ ...props
43
+ }: React.ComponentProps<"button"> &
44
+ VariantProps<typeof buttonVariants> & {
45
+ asChild?: boolean;
46
+ }) {
47
+ const Comp = asChild ? Slot : "button";
48
+
49
+ return (
50
+ <Comp
51
+ data-slot="button"
52
+ className={cn(buttonVariants({ variant, size, className }))}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ export { Button, buttonVariants };
src/app/components/ui/calendar.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ChevronLeft, ChevronRight } from "lucide-react";
5
+ import { DayPicker } from "react-day-picker";
6
+
7
+ import { cn } from "./utils";
8
+ import { buttonVariants } from "./button";
9
+
10
+ function Calendar({
11
+ className,
12
+ classNames,
13
+ showOutsideDays = true,
14
+ ...props
15
+ }: React.ComponentProps<typeof DayPicker>) {
16
+ return (
17
+ <DayPicker
18
+ showOutsideDays={showOutsideDays}
19
+ className={cn("p-3", className)}
20
+ classNames={{
21
+ months: "flex flex-col sm:flex-row gap-2",
22
+ month: "flex flex-col gap-4",
23
+ caption: "flex justify-center pt-1 relative items-center w-full",
24
+ caption_label: "text-sm font-medium",
25
+ nav: "flex items-center gap-1",
26
+ nav_button: cn(
27
+ buttonVariants({ variant: "outline" }),
28
+ "size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
29
+ ),
30
+ nav_button_previous: "absolute left-1",
31
+ nav_button_next: "absolute right-1",
32
+ table: "w-full border-collapse space-x-1",
33
+ head_row: "flex",
34
+ head_cell:
35
+ "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
36
+ row: "flex w-full mt-2",
37
+ cell: cn(
38
+ "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
39
+ props.mode === "range"
40
+ ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
41
+ : "[&:has([aria-selected])]:rounded-md",
42
+ ),
43
+ day: cn(
44
+ buttonVariants({ variant: "ghost" }),
45
+ "size-8 p-0 font-normal aria-selected:opacity-100",
46
+ ),
47
+ day_range_start:
48
+ "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
49
+ day_range_end:
50
+ "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
51
+ day_selected:
52
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
53
+ day_today: "bg-accent text-accent-foreground",
54
+ day_outside:
55
+ "day-outside text-muted-foreground aria-selected:text-muted-foreground",
56
+ day_disabled: "text-muted-foreground opacity-50",
57
+ day_range_middle:
58
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
59
+ day_hidden: "invisible",
60
+ ...classNames,
61
+ }}
62
+ components={{
63
+ IconLeft: ({ className, ...props }) => (
64
+ <ChevronLeft className={cn("size-4", className)} {...props} />
65
+ ),
66
+ IconRight: ({ className, ...props }) => (
67
+ <ChevronRight className={cn("size-4", className)} {...props} />
68
+ ),
69
+ }}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ export { Calendar };
src/app/components/ui/card.tsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "./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",
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-1.5 px-6 pt-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
+ <h4
34
+ data-slot="card-title"
35
+ className={cn("leading-none", className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <p
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground", 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 [&:last-child]:pb-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 pb-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
+ };
src/app/components/ui/carousel.tsx ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import useEmblaCarousel, {
5
+ type UseEmblaCarouselType,
6
+ } from "embla-carousel-react";
7
+ import { ArrowLeft, ArrowRight } from "lucide-react";
8
+
9
+ import { cn } from "./utils";
10
+ import { Button } from "./button";
11
+
12
+ type CarouselApi = UseEmblaCarouselType[1];
13
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
14
+ type CarouselOptions = UseCarouselParameters[0];
15
+ type CarouselPlugin = UseCarouselParameters[1];
16
+
17
+ type CarouselProps = {
18
+ opts?: CarouselOptions;
19
+ plugins?: CarouselPlugin;
20
+ orientation?: "horizontal" | "vertical";
21
+ setApi?: (api: CarouselApi) => void;
22
+ };
23
+
24
+ type CarouselContextProps = {
25
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
26
+ api: ReturnType<typeof useEmblaCarousel>[1];
27
+ scrollPrev: () => void;
28
+ scrollNext: () => void;
29
+ canScrollPrev: boolean;
30
+ canScrollNext: boolean;
31
+ } & CarouselProps;
32
+
33
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
34
+
35
+ function useCarousel() {
36
+ const context = React.useContext(CarouselContext);
37
+
38
+ if (!context) {
39
+ throw new Error("useCarousel must be used within a <Carousel />");
40
+ }
41
+
42
+ return context;
43
+ }
44
+
45
+ function Carousel({
46
+ orientation = "horizontal",
47
+ opts,
48
+ setApi,
49
+ plugins,
50
+ className,
51
+ children,
52
+ ...props
53
+ }: React.ComponentProps<"div"> & CarouselProps) {
54
+ const [carouselRef, api] = useEmblaCarousel(
55
+ {
56
+ ...opts,
57
+ axis: orientation === "horizontal" ? "x" : "y",
58
+ },
59
+ plugins,
60
+ );
61
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
62
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
63
+
64
+ const onSelect = React.useCallback((api: CarouselApi) => {
65
+ if (!api) return;
66
+ setCanScrollPrev(api.canScrollPrev());
67
+ setCanScrollNext(api.canScrollNext());
68
+ }, []);
69
+
70
+ const scrollPrev = React.useCallback(() => {
71
+ api?.scrollPrev();
72
+ }, [api]);
73
+
74
+ const scrollNext = React.useCallback(() => {
75
+ api?.scrollNext();
76
+ }, [api]);
77
+
78
+ const handleKeyDown = React.useCallback(
79
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
80
+ if (event.key === "ArrowLeft") {
81
+ event.preventDefault();
82
+ scrollPrev();
83
+ } else if (event.key === "ArrowRight") {
84
+ event.preventDefault();
85
+ scrollNext();
86
+ }
87
+ },
88
+ [scrollPrev, scrollNext],
89
+ );
90
+
91
+ React.useEffect(() => {
92
+ if (!api || !setApi) return;
93
+ setApi(api);
94
+ }, [api, setApi]);
95
+
96
+ React.useEffect(() => {
97
+ if (!api) return;
98
+ onSelect(api);
99
+ api.on("reInit", onSelect);
100
+ api.on("select", onSelect);
101
+
102
+ return () => {
103
+ api?.off("select", onSelect);
104
+ };
105
+ }, [api, onSelect]);
106
+
107
+ return (
108
+ <CarouselContext.Provider
109
+ value={{
110
+ carouselRef,
111
+ api: api,
112
+ opts,
113
+ orientation:
114
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
115
+ scrollPrev,
116
+ scrollNext,
117
+ canScrollPrev,
118
+ canScrollNext,
119
+ }}
120
+ >
121
+ <div
122
+ onKeyDownCapture={handleKeyDown}
123
+ className={cn("relative", className)}
124
+ role="region"
125
+ aria-roledescription="carousel"
126
+ data-slot="carousel"
127
+ {...props}
128
+ >
129
+ {children}
130
+ </div>
131
+ </CarouselContext.Provider>
132
+ );
133
+ }
134
+
135
+ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
136
+ const { carouselRef, orientation } = useCarousel();
137
+
138
+ return (
139
+ <div
140
+ ref={carouselRef}
141
+ className="overflow-hidden"
142
+ data-slot="carousel-content"
143
+ >
144
+ <div
145
+ className={cn(
146
+ "flex",
147
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
148
+ className,
149
+ )}
150
+ {...props}
151
+ />
152
+ </div>
153
+ );
154
+ }
155
+
156
+ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
157
+ const { orientation } = useCarousel();
158
+
159
+ return (
160
+ <div
161
+ role="group"
162
+ aria-roledescription="slide"
163
+ data-slot="carousel-item"
164
+ className={cn(
165
+ "min-w-0 shrink-0 grow-0 basis-full",
166
+ orientation === "horizontal" ? "pl-4" : "pt-4",
167
+ className,
168
+ )}
169
+ {...props}
170
+ />
171
+ );
172
+ }
173
+
174
+ function CarouselPrevious({
175
+ className,
176
+ variant = "outline",
177
+ size = "icon",
178
+ ...props
179
+ }: React.ComponentProps<typeof Button>) {
180
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
181
+
182
+ return (
183
+ <Button
184
+ data-slot="carousel-previous"
185
+ variant={variant}
186
+ size={size}
187
+ className={cn(
188
+ "absolute size-8 rounded-full",
189
+ orientation === "horizontal"
190
+ ? "top-1/2 -left-12 -translate-y-1/2"
191
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
192
+ className,
193
+ )}
194
+ disabled={!canScrollPrev}
195
+ onClick={scrollPrev}
196
+ {...props}
197
+ >
198
+ <ArrowLeft />
199
+ <span className="sr-only">Previous slide</span>
200
+ </Button>
201
+ );
202
+ }
203
+
204
+ function CarouselNext({
205
+ className,
206
+ variant = "outline",
207
+ size = "icon",
208
+ ...props
209
+ }: React.ComponentProps<typeof Button>) {
210
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
211
+
212
+ return (
213
+ <Button
214
+ data-slot="carousel-next"
215
+ variant={variant}
216
+ size={size}
217
+ className={cn(
218
+ "absolute size-8 rounded-full",
219
+ orientation === "horizontal"
220
+ ? "top-1/2 -right-12 -translate-y-1/2"
221
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
222
+ className,
223
+ )}
224
+ disabled={!canScrollNext}
225
+ onClick={scrollNext}
226
+ {...props}
227
+ >
228
+ <ArrowRight />
229
+ <span className="sr-only">Next slide</span>
230
+ </Button>
231
+ );
232
+ }
233
+
234
+ export {
235
+ type CarouselApi,
236
+ Carousel,
237
+ CarouselContent,
238
+ CarouselItem,
239
+ CarouselPrevious,
240
+ CarouselNext,
241
+ };
src/app/components/ui/chart.tsx ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as RechartsPrimitive from "recharts";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ // Format: { THEME_NAME: CSS_SELECTOR }
9
+ const THEMES = { light: "", dark: ".dark" } as const;
10
+
11
+ export type ChartConfig = {
12
+ [k in string]: {
13
+ label?: React.ReactNode;
14
+ icon?: React.ComponentType;
15
+ } & (
16
+ | { color?: string; theme?: never }
17
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
18
+ );
19
+ };
20
+
21
+ type ChartContextProps = {
22
+ config: ChartConfig;
23
+ };
24
+
25
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
26
+
27
+ function useChart() {
28
+ const context = React.useContext(ChartContext);
29
+
30
+ if (!context) {
31
+ throw new Error("useChart must be used within a <ChartContainer />");
32
+ }
33
+
34
+ return context;
35
+ }
36
+
37
+ function ChartContainer({
38
+ id,
39
+ className,
40
+ children,
41
+ config,
42
+ ...props
43
+ }: React.ComponentProps<"div"> & {
44
+ config: ChartConfig;
45
+ children: React.ComponentProps<
46
+ typeof RechartsPrimitive.ResponsiveContainer
47
+ >["children"];
48
+ }) {
49
+ const uniqueId = React.useId();
50
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
51
+
52
+ return (
53
+ <ChartContext.Provider value={{ config }}>
54
+ <div
55
+ data-slot="chart"
56
+ data-chart={chartId}
57
+ className={cn(
58
+ "[&_.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",
59
+ className,
60
+ )}
61
+ {...props}
62
+ >
63
+ <ChartStyle id={chartId} config={config} />
64
+ <RechartsPrimitive.ResponsiveContainer>
65
+ {children}
66
+ </RechartsPrimitive.ResponsiveContainer>
67
+ </div>
68
+ </ChartContext.Provider>
69
+ );
70
+ }
71
+
72
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
73
+ const colorConfig = Object.entries(config).filter(
74
+ ([, config]) => config.theme || config.color,
75
+ );
76
+
77
+ if (!colorConfig.length) {
78
+ return null;
79
+ }
80
+
81
+ return (
82
+ <style
83
+ dangerouslySetInnerHTML={{
84
+ __html: Object.entries(THEMES)
85
+ .map(
86
+ ([theme, prefix]) => `
87
+ ${prefix} [data-chart=${id}] {
88
+ ${colorConfig
89
+ .map(([key, itemConfig]) => {
90
+ const color =
91
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
92
+ itemConfig.color;
93
+ return color ? ` --color-${key}: ${color};` : null;
94
+ })
95
+ .join("\n")}
96
+ }
97
+ `,
98
+ )
99
+ .join("\n"),
100
+ }}
101
+ />
102
+ );
103
+ };
104
+
105
+ const ChartTooltip = RechartsPrimitive.Tooltip;
106
+
107
+ function ChartTooltipContent({
108
+ active,
109
+ payload,
110
+ className,
111
+ indicator = "dot",
112
+ hideLabel = false,
113
+ hideIndicator = false,
114
+ label,
115
+ labelFormatter,
116
+ labelClassName,
117
+ formatter,
118
+ color,
119
+ nameKey,
120
+ labelKey,
121
+ }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
122
+ React.ComponentProps<"div"> & {
123
+ hideLabel?: boolean;
124
+ hideIndicator?: boolean;
125
+ indicator?: "line" | "dot" | "dashed";
126
+ nameKey?: string;
127
+ labelKey?: string;
128
+ }) {
129
+ const { config } = useChart();
130
+
131
+ const tooltipLabel = React.useMemo(() => {
132
+ if (hideLabel || !payload?.length) {
133
+ return null;
134
+ }
135
+
136
+ const [item] = payload;
137
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
138
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
139
+ const value =
140
+ !labelKey && typeof label === "string"
141
+ ? config[label as keyof typeof config]?.label || label
142
+ : itemConfig?.label;
143
+
144
+ if (labelFormatter) {
145
+ return (
146
+ <div className={cn("font-medium", labelClassName)}>
147
+ {labelFormatter(value, payload)}
148
+ </div>
149
+ );
150
+ }
151
+
152
+ if (!value) {
153
+ return null;
154
+ }
155
+
156
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
157
+ }, [
158
+ label,
159
+ labelFormatter,
160
+ payload,
161
+ hideLabel,
162
+ labelClassName,
163
+ config,
164
+ labelKey,
165
+ ]);
166
+
167
+ if (!active || !payload?.length) {
168
+ return null;
169
+ }
170
+
171
+ const nestLabel = payload.length === 1 && indicator !== "dot";
172
+
173
+ return (
174
+ <div
175
+ className={cn(
176
+ "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",
177
+ className,
178
+ )}
179
+ >
180
+ {!nestLabel ? tooltipLabel : null}
181
+ <div className="grid gap-1.5">
182
+ {payload.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.map((item) => {
279
+ const key = `${nameKey || item.dataKey || "value"}`;
280
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
281
+
282
+ return (
283
+ <div
284
+ key={item.value}
285
+ className={cn(
286
+ "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
287
+ )}
288
+ >
289
+ {itemConfig?.icon && !hideIcon ? (
290
+ <itemConfig.icon />
291
+ ) : (
292
+ <div
293
+ className="h-2 w-2 shrink-0 rounded-[2px]"
294
+ style={{
295
+ backgroundColor: item.color,
296
+ }}
297
+ />
298
+ )}
299
+ {itemConfig?.label}
300
+ </div>
301
+ );
302
+ })}
303
+ </div>
304
+ );
305
+ }
306
+
307
+ // Helper to extract item config from a payload.
308
+ function getPayloadConfigFromPayload(
309
+ config: ChartConfig,
310
+ payload: unknown,
311
+ key: string,
312
+ ) {
313
+ if (typeof payload !== "object" || payload === null) {
314
+ return undefined;
315
+ }
316
+
317
+ const payloadPayload =
318
+ "payload" in payload &&
319
+ typeof payload.payload === "object" &&
320
+ payload.payload !== null
321
+ ? payload.payload
322
+ : undefined;
323
+
324
+ let configLabelKey: string = key;
325
+
326
+ if (
327
+ key in payload &&
328
+ typeof payload[key as keyof typeof payload] === "string"
329
+ ) {
330
+ configLabelKey = payload[key as keyof typeof payload] as string;
331
+ } else if (
332
+ payloadPayload &&
333
+ key in payloadPayload &&
334
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
335
+ ) {
336
+ configLabelKey = payloadPayload[
337
+ key as keyof typeof payloadPayload
338
+ ] as string;
339
+ }
340
+
341
+ return configLabelKey in config
342
+ ? config[configLabelKey]
343
+ : config[key as keyof typeof config];
344
+ }
345
+
346
+ export {
347
+ ChartContainer,
348
+ ChartTooltip,
349
+ ChartTooltipContent,
350
+ ChartLegend,
351
+ ChartLegendContent,
352
+ ChartStyle,
353
+ };
src/app/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5
+ import { CheckIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function Checkbox({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
13
+ return (
14
+ <CheckboxPrimitive.Root
15
+ data-slot="checkbox"
16
+ className={cn(
17
+ "peer border bg-input-background 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",
18
+ className,
19
+ )}
20
+ {...props}
21
+ >
22
+ <CheckboxPrimitive.Indicator
23
+ data-slot="checkbox-indicator"
24
+ className="flex items-center justify-center text-current transition-none"
25
+ >
26
+ <CheckIcon className="size-3.5" />
27
+ </CheckboxPrimitive.Indicator>
28
+ </CheckboxPrimitive.Root>
29
+ );
30
+ }
31
+
32
+ export { Checkbox };
src/app/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4
+
5
+ function Collapsible({
6
+ ...props
7
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
8
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
9
+ }
10
+
11
+ function CollapsibleTrigger({
12
+ ...props
13
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
14
+ return (
15
+ <CollapsiblePrimitive.CollapsibleTrigger
16
+ data-slot="collapsible-trigger"
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function CollapsibleContent({
23
+ ...props
24
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
25
+ return (
26
+ <CollapsiblePrimitive.CollapsibleContent
27
+ data-slot="collapsible-content"
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
src/app/components/ui/command.tsx ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 "./utils";
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from "./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
+ ...props
37
+ }: React.ComponentProps<typeof Dialog> & {
38
+ title?: string;
39
+ description?: string;
40
+ }) {
41
+ return (
42
+ <Dialog {...props}>
43
+ <DialogHeader className="sr-only">
44
+ <DialogTitle>{title}</DialogTitle>
45
+ <DialogDescription>{description}</DialogDescription>
46
+ </DialogHeader>
47
+ <DialogContent className="overflow-hidden p-0">
48
+ <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">
49
+ {children}
50
+ </Command>
51
+ </DialogContent>
52
+ </Dialog>
53
+ );
54
+ }
55
+
56
+ function CommandInput({
57
+ className,
58
+ ...props
59
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
60
+ return (
61
+ <div
62
+ data-slot="command-input-wrapper"
63
+ className="flex h-9 items-center gap-2 border-b px-3"
64
+ >
65
+ <SearchIcon className="size-4 shrink-0 opacity-50" />
66
+ <CommandPrimitive.Input
67
+ data-slot="command-input"
68
+ className={cn(
69
+ "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",
70
+ className,
71
+ )}
72
+ {...props}
73
+ />
74
+ </div>
75
+ );
76
+ }
77
+
78
+ function CommandList({
79
+ className,
80
+ ...props
81
+ }: React.ComponentProps<typeof CommandPrimitive.List>) {
82
+ return (
83
+ <CommandPrimitive.List
84
+ data-slot="command-list"
85
+ className={cn(
86
+ "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+
94
+ function CommandEmpty({
95
+ ...props
96
+ }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
97
+ return (
98
+ <CommandPrimitive.Empty
99
+ data-slot="command-empty"
100
+ className="py-6 text-center text-sm"
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ function CommandGroup({
107
+ className,
108
+ ...props
109
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
110
+ return (
111
+ <CommandPrimitive.Group
112
+ data-slot="command-group"
113
+ className={cn(
114
+ "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",
115
+ className,
116
+ )}
117
+ {...props}
118
+ />
119
+ );
120
+ }
121
+
122
+ function CommandSeparator({
123
+ className,
124
+ ...props
125
+ }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
126
+ return (
127
+ <CommandPrimitive.Separator
128
+ data-slot="command-separator"
129
+ className={cn("bg-border -mx-1 h-px", className)}
130
+ {...props}
131
+ />
132
+ );
133
+ }
134
+
135
+ function CommandItem({
136
+ className,
137
+ ...props
138
+ }: React.ComponentProps<typeof CommandPrimitive.Item>) {
139
+ return (
140
+ <CommandPrimitive.Item
141
+ data-slot="command-item"
142
+ className={cn(
143
+ "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",
144
+ className,
145
+ )}
146
+ {...props}
147
+ />
148
+ );
149
+ }
150
+
151
+ function CommandShortcut({
152
+ className,
153
+ ...props
154
+ }: React.ComponentProps<"span">) {
155
+ return (
156
+ <span
157
+ data-slot="command-shortcut"
158
+ className={cn(
159
+ "text-muted-foreground ml-auto text-xs tracking-widest",
160
+ className,
161
+ )}
162
+ {...props}
163
+ />
164
+ );
165
+ }
166
+
167
+ export {
168
+ Command,
169
+ CommandDialog,
170
+ CommandInput,
171
+ CommandList,
172
+ CommandEmpty,
173
+ CommandGroup,
174
+ CommandItem,
175
+ CommandShortcut,
176
+ CommandSeparator,
177
+ };
src/app/components/ui/context-menu.tsx ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function ContextMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
12
+ return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
13
+ }
14
+
15
+ function ContextMenuTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
18
+ return (
19
+ <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
20
+ );
21
+ }
22
+
23
+ function ContextMenuGroup({
24
+ ...props
25
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
26
+ return (
27
+ <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
28
+ );
29
+ }
30
+
31
+ function ContextMenuPortal({
32
+ ...props
33
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
34
+ return (
35
+ <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
36
+ );
37
+ }
38
+
39
+ function ContextMenuSub({
40
+ ...props
41
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
42
+ return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
43
+ }
44
+
45
+ function ContextMenuRadioGroup({
46
+ ...props
47
+ }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
48
+ return (
49
+ <ContextMenuPrimitive.RadioGroup
50
+ data-slot="context-menu-radio-group"
51
+ {...props}
52
+ />
53
+ );
54
+ }
55
+
56
+ function ContextMenuSubTrigger({
57
+ className,
58
+ inset,
59
+ children,
60
+ ...props
61
+ }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
62
+ inset?: boolean;
63
+ }) {
64
+ return (
65
+ <ContextMenuPrimitive.SubTrigger
66
+ data-slot="context-menu-sub-trigger"
67
+ data-inset={inset}
68
+ className={cn(
69
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-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",
70
+ className,
71
+ )}
72
+ {...props}
73
+ >
74
+ {children}
75
+ <ChevronRightIcon className="ml-auto" />
76
+ </ContextMenuPrimitive.SubTrigger>
77
+ );
78
+ }
79
+
80
+ function ContextMenuSubContent({
81
+ className,
82
+ ...props
83
+ }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
84
+ return (
85
+ <ContextMenuPrimitive.SubContent
86
+ data-slot="context-menu-sub-content"
87
+ className={cn(
88
+ "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",
89
+ className,
90
+ )}
91
+ {...props}
92
+ />
93
+ );
94
+ }
95
+
96
+ function ContextMenuContent({
97
+ className,
98
+ ...props
99
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
100
+ return (
101
+ <ContextMenuPrimitive.Portal>
102
+ <ContextMenuPrimitive.Content
103
+ data-slot="context-menu-content"
104
+ className={cn(
105
+ "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",
106
+ className,
107
+ )}
108
+ {...props}
109
+ />
110
+ </ContextMenuPrimitive.Portal>
111
+ );
112
+ }
113
+
114
+ function ContextMenuItem({
115
+ className,
116
+ inset,
117
+ variant = "default",
118
+ ...props
119
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
120
+ inset?: boolean;
121
+ variant?: "default" | "destructive";
122
+ }) {
123
+ return (
124
+ <ContextMenuPrimitive.Item
125
+ data-slot="context-menu-item"
126
+ data-inset={inset}
127
+ data-variant={variant}
128
+ className={cn(
129
+ "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",
130
+ className,
131
+ )}
132
+ {...props}
133
+ />
134
+ );
135
+ }
136
+
137
+ function ContextMenuCheckboxItem({
138
+ className,
139
+ children,
140
+ checked,
141
+ ...props
142
+ }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
143
+ return (
144
+ <ContextMenuPrimitive.CheckboxItem
145
+ data-slot="context-menu-checkbox-item"
146
+ className={cn(
147
+ "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",
148
+ className,
149
+ )}
150
+ checked={checked}
151
+ {...props}
152
+ >
153
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
154
+ <ContextMenuPrimitive.ItemIndicator>
155
+ <CheckIcon className="size-4" />
156
+ </ContextMenuPrimitive.ItemIndicator>
157
+ </span>
158
+ {children}
159
+ </ContextMenuPrimitive.CheckboxItem>
160
+ );
161
+ }
162
+
163
+ function ContextMenuRadioItem({
164
+ className,
165
+ children,
166
+ ...props
167
+ }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
168
+ return (
169
+ <ContextMenuPrimitive.RadioItem
170
+ data-slot="context-menu-radio-item"
171
+ className={cn(
172
+ "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",
173
+ className,
174
+ )}
175
+ {...props}
176
+ >
177
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
178
+ <ContextMenuPrimitive.ItemIndicator>
179
+ <CircleIcon className="size-2 fill-current" />
180
+ </ContextMenuPrimitive.ItemIndicator>
181
+ </span>
182
+ {children}
183
+ </ContextMenuPrimitive.RadioItem>
184
+ );
185
+ }
186
+
187
+ function ContextMenuLabel({
188
+ className,
189
+ inset,
190
+ ...props
191
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
192
+ inset?: boolean;
193
+ }) {
194
+ return (
195
+ <ContextMenuPrimitive.Label
196
+ data-slot="context-menu-label"
197
+ data-inset={inset}
198
+ className={cn(
199
+ "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
200
+ className,
201
+ )}
202
+ {...props}
203
+ />
204
+ );
205
+ }
206
+
207
+ function ContextMenuSeparator({
208
+ className,
209
+ ...props
210
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
211
+ return (
212
+ <ContextMenuPrimitive.Separator
213
+ data-slot="context-menu-separator"
214
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
215
+ {...props}
216
+ />
217
+ );
218
+ }
219
+
220
+ function ContextMenuShortcut({
221
+ className,
222
+ ...props
223
+ }: React.ComponentProps<"span">) {
224
+ return (
225
+ <span
226
+ data-slot="context-menu-shortcut"
227
+ className={cn(
228
+ "text-muted-foreground ml-auto text-xs tracking-widest",
229
+ className,
230
+ )}
231
+ {...props}
232
+ />
233
+ );
234
+ }
235
+
236
+ export {
237
+ ContextMenu,
238
+ ContextMenuTrigger,
239
+ ContextMenuContent,
240
+ ContextMenuItem,
241
+ ContextMenuCheckboxItem,
242
+ ContextMenuRadioItem,
243
+ ContextMenuLabel,
244
+ ContextMenuSeparator,
245
+ ContextMenuShortcut,
246
+ ContextMenuGroup,
247
+ ContextMenuPortal,
248
+ ContextMenuSub,
249
+ ContextMenuSubContent,
250
+ ContextMenuSubTrigger,
251
+ ContextMenuRadioGroup,
252
+ };
src/app/components/ui/dialog.tsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { XIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function Dialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
12
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
13
+ }
14
+
15
+ function DialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
18
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
19
+ }
20
+
21
+ function DialogPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
24
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
25
+ }
26
+
27
+ function DialogClose({
28
+ ...props
29
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
30
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
31
+ }
32
+
33
+ function DialogOverlay({
34
+ className,
35
+ ...props
36
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
37
+ return (
38
+ <DialogPrimitive.Overlay
39
+ data-slot="dialog-overlay"
40
+ className={cn(
41
+ "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",
42
+ className,
43
+ )}
44
+ {...props}
45
+ />
46
+ );
47
+ }
48
+
49
+ function DialogContent({
50
+ className,
51
+ children,
52
+ ...props
53
+ }: React.ComponentProps<typeof DialogPrimitive.Content>) {
54
+ return (
55
+ <DialogPortal data-slot="dialog-portal">
56
+ <DialogOverlay />
57
+ <DialogPrimitive.Content
58
+ data-slot="dialog-content"
59
+ className={cn(
60
+ "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",
61
+ className,
62
+ )}
63
+ {...props}
64
+ >
65
+ {children}
66
+ <DialogPrimitive.Close 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">
67
+ <XIcon />
68
+ <span className="sr-only">Close</span>
69
+ </DialogPrimitive.Close>
70
+ </DialogPrimitive.Content>
71
+ </DialogPortal>
72
+ );
73
+ }
74
+
75
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
76
+ return (
77
+ <div
78
+ data-slot="dialog-header"
79
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
86
+ return (
87
+ <div
88
+ data-slot="dialog-footer"
89
+ className={cn(
90
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
91
+ className,
92
+ )}
93
+ {...props}
94
+ />
95
+ );
96
+ }
97
+
98
+ function DialogTitle({
99
+ className,
100
+ ...props
101
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
102
+ return (
103
+ <DialogPrimitive.Title
104
+ data-slot="dialog-title"
105
+ className={cn("text-lg leading-none font-semibold", className)}
106
+ {...props}
107
+ />
108
+ );
109
+ }
110
+
111
+ function DialogDescription({
112
+ className,
113
+ ...props
114
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
115
+ return (
116
+ <DialogPrimitive.Description
117
+ data-slot="dialog-description"
118
+ className={cn("text-muted-foreground text-sm", className)}
119
+ {...props}
120
+ />
121
+ );
122
+ }
123
+
124
+ export {
125
+ Dialog,
126
+ DialogClose,
127
+ DialogContent,
128
+ DialogDescription,
129
+ DialogFooter,
130
+ DialogHeader,
131
+ DialogOverlay,
132
+ DialogPortal,
133
+ DialogTitle,
134
+ DialogTrigger,
135
+ };
src/app/components/ui/drawer.tsx ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Drawer as DrawerPrimitive } from "vaul";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function Drawer({
9
+ ...props
10
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
11
+ return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
12
+ }
13
+
14
+ function DrawerTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
17
+ return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
18
+ }
19
+
20
+ function DrawerPortal({
21
+ ...props
22
+ }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
23
+ return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
24
+ }
25
+
26
+ function DrawerClose({
27
+ ...props
28
+ }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
29
+ return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
30
+ }
31
+
32
+ function DrawerOverlay({
33
+ className,
34
+ ...props
35
+ }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
36
+ return (
37
+ <DrawerPrimitive.Overlay
38
+ data-slot="drawer-overlay"
39
+ className={cn(
40
+ "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",
41
+ className,
42
+ )}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+
48
+ function DrawerContent({
49
+ className,
50
+ children,
51
+ ...props
52
+ }: React.ComponentProps<typeof DrawerPrimitive.Content>) {
53
+ return (
54
+ <DrawerPortal data-slot="drawer-portal">
55
+ <DrawerOverlay />
56
+ <DrawerPrimitive.Content
57
+ data-slot="drawer-content"
58
+ className={cn(
59
+ "group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
60
+ "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",
61
+ "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",
62
+ "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",
63
+ "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",
64
+ className,
65
+ )}
66
+ {...props}
67
+ >
68
+ <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" />
69
+ {children}
70
+ </DrawerPrimitive.Content>
71
+ </DrawerPortal>
72
+ );
73
+ }
74
+
75
+ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
76
+ return (
77
+ <div
78
+ data-slot="drawer-header"
79
+ className={cn("flex flex-col gap-1.5 p-4", className)}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
86
+ return (
87
+ <div
88
+ data-slot="drawer-footer"
89
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function DrawerTitle({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
99
+ return (
100
+ <DrawerPrimitive.Title
101
+ data-slot="drawer-title"
102
+ className={cn("text-foreground font-semibold", className)}
103
+ {...props}
104
+ />
105
+ );
106
+ }
107
+
108
+ function DrawerDescription({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof DrawerPrimitive.Description>) {
112
+ return (
113
+ <DrawerPrimitive.Description
114
+ data-slot="drawer-description"
115
+ className={cn("text-muted-foreground text-sm", className)}
116
+ {...props}
117
+ />
118
+ );
119
+ }
120
+
121
+ export {
122
+ Drawer,
123
+ DrawerPortal,
124
+ DrawerOverlay,
125
+ DrawerTrigger,
126
+ DrawerClose,
127
+ DrawerContent,
128
+ DrawerHeader,
129
+ DrawerFooter,
130
+ DrawerTitle,
131
+ DrawerDescription,
132
+ };
src/app/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function DropdownMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
12
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
13
+ }
14
+
15
+ function DropdownMenuPortal({
16
+ ...props
17
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
18
+ return (
19
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
20
+ );
21
+ }
22
+
23
+ function DropdownMenuTrigger({
24
+ ...props
25
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
26
+ return (
27
+ <DropdownMenuPrimitive.Trigger
28
+ data-slot="dropdown-menu-trigger"
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+
34
+ function DropdownMenuContent({
35
+ className,
36
+ sideOffset = 4,
37
+ ...props
38
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
39
+ return (
40
+ <DropdownMenuPrimitive.Portal>
41
+ <DropdownMenuPrimitive.Content
42
+ data-slot="dropdown-menu-content"
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ "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",
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ </DropdownMenuPrimitive.Portal>
51
+ );
52
+ }
53
+
54
+ function DropdownMenuGroup({
55
+ ...props
56
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
57
+ return (
58
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
59
+ );
60
+ }
61
+
62
+ function DropdownMenuItem({
63
+ className,
64
+ inset,
65
+ variant = "default",
66
+ ...props
67
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
68
+ inset?: boolean;
69
+ variant?: "default" | "destructive";
70
+ }) {
71
+ return (
72
+ <DropdownMenuPrimitive.Item
73
+ data-slot="dropdown-menu-item"
74
+ data-inset={inset}
75
+ data-variant={variant}
76
+ className={cn(
77
+ "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",
78
+ className,
79
+ )}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ function DropdownMenuCheckboxItem({
86
+ className,
87
+ children,
88
+ checked,
89
+ ...props
90
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
91
+ return (
92
+ <DropdownMenuPrimitive.CheckboxItem
93
+ data-slot="dropdown-menu-checkbox-item"
94
+ className={cn(
95
+ "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",
96
+ className,
97
+ )}
98
+ checked={checked}
99
+ {...props}
100
+ >
101
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
102
+ <DropdownMenuPrimitive.ItemIndicator>
103
+ <CheckIcon className="size-4" />
104
+ </DropdownMenuPrimitive.ItemIndicator>
105
+ </span>
106
+ {children}
107
+ </DropdownMenuPrimitive.CheckboxItem>
108
+ );
109
+ }
110
+
111
+ function DropdownMenuRadioGroup({
112
+ ...props
113
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
114
+ return (
115
+ <DropdownMenuPrimitive.RadioGroup
116
+ data-slot="dropdown-menu-radio-group"
117
+ {...props}
118
+ />
119
+ );
120
+ }
121
+
122
+ function DropdownMenuRadioItem({
123
+ className,
124
+ children,
125
+ ...props
126
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
127
+ return (
128
+ <DropdownMenuPrimitive.RadioItem
129
+ data-slot="dropdown-menu-radio-item"
130
+ className={cn(
131
+ "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",
132
+ className,
133
+ )}
134
+ {...props}
135
+ >
136
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
137
+ <DropdownMenuPrimitive.ItemIndicator>
138
+ <CircleIcon className="size-2 fill-current" />
139
+ </DropdownMenuPrimitive.ItemIndicator>
140
+ </span>
141
+ {children}
142
+ </DropdownMenuPrimitive.RadioItem>
143
+ );
144
+ }
145
+
146
+ function DropdownMenuLabel({
147
+ className,
148
+ inset,
149
+ ...props
150
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
151
+ inset?: boolean;
152
+ }) {
153
+ return (
154
+ <DropdownMenuPrimitive.Label
155
+ data-slot="dropdown-menu-label"
156
+ data-inset={inset}
157
+ className={cn(
158
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
159
+ className,
160
+ )}
161
+ {...props}
162
+ />
163
+ );
164
+ }
165
+
166
+ function DropdownMenuSeparator({
167
+ className,
168
+ ...props
169
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
170
+ return (
171
+ <DropdownMenuPrimitive.Separator
172
+ data-slot="dropdown-menu-separator"
173
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
174
+ {...props}
175
+ />
176
+ );
177
+ }
178
+
179
+ function DropdownMenuShortcut({
180
+ className,
181
+ ...props
182
+ }: React.ComponentProps<"span">) {
183
+ return (
184
+ <span
185
+ data-slot="dropdown-menu-shortcut"
186
+ className={cn(
187
+ "text-muted-foreground ml-auto text-xs tracking-widest",
188
+ className,
189
+ )}
190
+ {...props}
191
+ />
192
+ );
193
+ }
194
+
195
+ function DropdownMenuSub({
196
+ ...props
197
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
198
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
199
+ }
200
+
201
+ function DropdownMenuSubTrigger({
202
+ className,
203
+ inset,
204
+ children,
205
+ ...props
206
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
207
+ inset?: boolean;
208
+ }) {
209
+ return (
210
+ <DropdownMenuPrimitive.SubTrigger
211
+ data-slot="dropdown-menu-sub-trigger"
212
+ data-inset={inset}
213
+ className={cn(
214
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
215
+ className,
216
+ )}
217
+ {...props}
218
+ >
219
+ {children}
220
+ <ChevronRightIcon className="ml-auto size-4" />
221
+ </DropdownMenuPrimitive.SubTrigger>
222
+ );
223
+ }
224
+
225
+ function DropdownMenuSubContent({
226
+ className,
227
+ ...props
228
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
229
+ return (
230
+ <DropdownMenuPrimitive.SubContent
231
+ data-slot="dropdown-menu-sub-content"
232
+ className={cn(
233
+ "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",
234
+ className,
235
+ )}
236
+ {...props}
237
+ />
238
+ );
239
+ }
240
+
241
+ export {
242
+ DropdownMenu,
243
+ DropdownMenuPortal,
244
+ DropdownMenuTrigger,
245
+ DropdownMenuContent,
246
+ DropdownMenuGroup,
247
+ DropdownMenuLabel,
248
+ DropdownMenuItem,
249
+ DropdownMenuCheckboxItem,
250
+ DropdownMenuRadioGroup,
251
+ DropdownMenuRadioItem,
252
+ DropdownMenuSeparator,
253
+ DropdownMenuShortcut,
254
+ DropdownMenuSub,
255
+ DropdownMenuSubTrigger,
256
+ DropdownMenuSubContent,
257
+ };
src/app/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 "./utils";
17
+ import { Label } from "./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
+ };
src/app/components/ui/hover-card.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function HoverCard({
9
+ ...props
10
+ }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
11
+ return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
12
+ }
13
+
14
+ function HoverCardTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
17
+ return (
18
+ <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
19
+ );
20
+ }
21
+
22
+ function HoverCardContent({
23
+ className,
24
+ align = "center",
25
+ sideOffset = 4,
26
+ ...props
27
+ }: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
28
+ return (
29
+ <HoverCardPrimitive.Portal data-slot="hover-card-portal">
30
+ <HoverCardPrimitive.Content
31
+ data-slot="hover-card-content"
32
+ align={align}
33
+ sideOffset={sideOffset}
34
+ className={cn(
35
+ "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",
36
+ className,
37
+ )}
38
+ {...props}
39
+ />
40
+ </HoverCardPrimitive.Portal>
41
+ );
42
+ }
43
+
44
+ export { HoverCard, HoverCardTrigger, HoverCardContent };
src/app/components/ui/input-otp.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { OTPInput, OTPInputContext } from "input-otp";
5
+ import { MinusIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function InputOTP({
10
+ className,
11
+ containerClassName,
12
+ ...props
13
+ }: React.ComponentProps<typeof OTPInput> & {
14
+ containerClassName?: string;
15
+ }) {
16
+ return (
17
+ <OTPInput
18
+ data-slot="input-otp"
19
+ containerClassName={cn(
20
+ "flex items-center gap-2 has-disabled:opacity-50",
21
+ containerClassName,
22
+ )}
23
+ className={cn("disabled:cursor-not-allowed", className)}
24
+ {...props}
25
+ />
26
+ );
27
+ }
28
+
29
+ function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
30
+ return (
31
+ <div
32
+ data-slot="input-otp-group"
33
+ className={cn("flex items-center gap-1", className)}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function InputOTPSlot({
40
+ index,
41
+ className,
42
+ ...props
43
+ }: React.ComponentProps<"div"> & {
44
+ index: number;
45
+ }) {
46
+ const inputOTPContext = React.useContext(OTPInputContext);
47
+ const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
48
+
49
+ return (
50
+ <div
51
+ data-slot="input-otp-slot"
52
+ data-active={isActive}
53
+ className={cn(
54
+ "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 bg-input-background 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]",
55
+ className,
56
+ )}
57
+ {...props}
58
+ >
59
+ {char}
60
+ {hasFakeCaret && (
61
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
62
+ <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
63
+ </div>
64
+ )}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
70
+ return (
71
+ <div data-slot="input-otp-separator" role="separator" {...props}>
72
+ <MinusIcon />
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
src/app/components/ui/input.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "./utils";
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base bg-input-background 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",
12
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ export { Input };
src/app/components/ui/label.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as LabelPrimitive from "@radix-ui/react-label";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function Label({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ data-slot="label"
15
+ className={cn(
16
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ export { Label };
src/app/components/ui/menubar.tsx ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as MenubarPrimitive from "@radix-ui/react-menubar";
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function Menubar({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof MenubarPrimitive.Root>) {
13
+ return (
14
+ <MenubarPrimitive.Root
15
+ data-slot="menubar"
16
+ className={cn(
17
+ "bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ function MenubarMenu({
26
+ ...props
27
+ }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
28
+ return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
29
+ }
30
+
31
+ function MenubarGroup({
32
+ ...props
33
+ }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
34
+ return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
35
+ }
36
+
37
+ function MenubarPortal({
38
+ ...props
39
+ }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
40
+ return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
41
+ }
42
+
43
+ function MenubarRadioGroup({
44
+ ...props
45
+ }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
46
+ return (
47
+ <MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
48
+ );
49
+ }
50
+
51
+ function MenubarTrigger({
52
+ className,
53
+ ...props
54
+ }: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
55
+ return (
56
+ <MenubarPrimitive.Trigger
57
+ data-slot="menubar-trigger"
58
+ className={cn(
59
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
60
+ className,
61
+ )}
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ function MenubarContent({
68
+ className,
69
+ align = "start",
70
+ alignOffset = -4,
71
+ sideOffset = 8,
72
+ ...props
73
+ }: React.ComponentProps<typeof MenubarPrimitive.Content>) {
74
+ return (
75
+ <MenubarPortal>
76
+ <MenubarPrimitive.Content
77
+ data-slot="menubar-content"
78
+ align={align}
79
+ alignOffset={alignOffset}
80
+ sideOffset={sideOffset}
81
+ className={cn(
82
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
83
+ className,
84
+ )}
85
+ {...props}
86
+ />
87
+ </MenubarPortal>
88
+ );
89
+ }
90
+
91
+ function MenubarItem({
92
+ className,
93
+ inset,
94
+ variant = "default",
95
+ ...props
96
+ }: React.ComponentProps<typeof MenubarPrimitive.Item> & {
97
+ inset?: boolean;
98
+ variant?: "default" | "destructive";
99
+ }) {
100
+ return (
101
+ <MenubarPrimitive.Item
102
+ data-slot="menubar-item"
103
+ data-inset={inset}
104
+ data-variant={variant}
105
+ className={cn(
106
+ "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",
107
+ className,
108
+ )}
109
+ {...props}
110
+ />
111
+ );
112
+ }
113
+
114
+ function MenubarCheckboxItem({
115
+ className,
116
+ children,
117
+ checked,
118
+ ...props
119
+ }: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
120
+ return (
121
+ <MenubarPrimitive.CheckboxItem
122
+ data-slot="menubar-checkbox-item"
123
+ className={cn(
124
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs 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",
125
+ className,
126
+ )}
127
+ checked={checked}
128
+ {...props}
129
+ >
130
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
131
+ <MenubarPrimitive.ItemIndicator>
132
+ <CheckIcon className="size-4" />
133
+ </MenubarPrimitive.ItemIndicator>
134
+ </span>
135
+ {children}
136
+ </MenubarPrimitive.CheckboxItem>
137
+ );
138
+ }
139
+
140
+ function MenubarRadioItem({
141
+ className,
142
+ children,
143
+ ...props
144
+ }: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
145
+ return (
146
+ <MenubarPrimitive.RadioItem
147
+ data-slot="menubar-radio-item"
148
+ className={cn(
149
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs 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",
150
+ className,
151
+ )}
152
+ {...props}
153
+ >
154
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
155
+ <MenubarPrimitive.ItemIndicator>
156
+ <CircleIcon className="size-2 fill-current" />
157
+ </MenubarPrimitive.ItemIndicator>
158
+ </span>
159
+ {children}
160
+ </MenubarPrimitive.RadioItem>
161
+ );
162
+ }
163
+
164
+ function MenubarLabel({
165
+ className,
166
+ inset,
167
+ ...props
168
+ }: React.ComponentProps<typeof MenubarPrimitive.Label> & {
169
+ inset?: boolean;
170
+ }) {
171
+ return (
172
+ <MenubarPrimitive.Label
173
+ data-slot="menubar-label"
174
+ data-inset={inset}
175
+ className={cn(
176
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
177
+ className,
178
+ )}
179
+ {...props}
180
+ />
181
+ );
182
+ }
183
+
184
+ function MenubarSeparator({
185
+ className,
186
+ ...props
187
+ }: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
188
+ return (
189
+ <MenubarPrimitive.Separator
190
+ data-slot="menubar-separator"
191
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
192
+ {...props}
193
+ />
194
+ );
195
+ }
196
+
197
+ function MenubarShortcut({
198
+ className,
199
+ ...props
200
+ }: React.ComponentProps<"span">) {
201
+ return (
202
+ <span
203
+ data-slot="menubar-shortcut"
204
+ className={cn(
205
+ "text-muted-foreground ml-auto text-xs tracking-widest",
206
+ className,
207
+ )}
208
+ {...props}
209
+ />
210
+ );
211
+ }
212
+
213
+ function MenubarSub({
214
+ ...props
215
+ }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
216
+ return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
217
+ }
218
+
219
+ function MenubarSubTrigger({
220
+ className,
221
+ inset,
222
+ children,
223
+ ...props
224
+ }: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
225
+ inset?: boolean;
226
+ }) {
227
+ return (
228
+ <MenubarPrimitive.SubTrigger
229
+ data-slot="menubar-sub-trigger"
230
+ data-inset={inset}
231
+ className={cn(
232
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
233
+ className,
234
+ )}
235
+ {...props}
236
+ >
237
+ {children}
238
+ <ChevronRightIcon className="ml-auto h-4 w-4" />
239
+ </MenubarPrimitive.SubTrigger>
240
+ );
241
+ }
242
+
243
+ function MenubarSubContent({
244
+ className,
245
+ ...props
246
+ }: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
247
+ return (
248
+ <MenubarPrimitive.SubContent
249
+ data-slot="menubar-sub-content"
250
+ className={cn(
251
+ "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-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
252
+ className,
253
+ )}
254
+ {...props}
255
+ />
256
+ );
257
+ }
258
+
259
+ export {
260
+ Menubar,
261
+ MenubarPortal,
262
+ MenubarMenu,
263
+ MenubarTrigger,
264
+ MenubarContent,
265
+ MenubarGroup,
266
+ MenubarSeparator,
267
+ MenubarLabel,
268
+ MenubarItem,
269
+ MenubarShortcut,
270
+ MenubarCheckboxItem,
271
+ MenubarRadioGroup,
272
+ MenubarRadioItem,
273
+ MenubarSub,
274
+ MenubarSubTrigger,
275
+ MenubarSubContent,
276
+ };
src/app/components/ui/navigation-menu.tsx ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
3
+ import { cva } from "class-variance-authority";
4
+ import { ChevronDownIcon } from "lucide-react";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function NavigationMenu({
9
+ className,
10
+ children,
11
+ viewport = true,
12
+ ...props
13
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
14
+ viewport?: boolean;
15
+ }) {
16
+ return (
17
+ <NavigationMenuPrimitive.Root
18
+ data-slot="navigation-menu"
19
+ data-viewport={viewport}
20
+ className={cn(
21
+ "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
22
+ className,
23
+ )}
24
+ {...props}
25
+ >
26
+ {children}
27
+ {viewport && <NavigationMenuViewport />}
28
+ </NavigationMenuPrimitive.Root>
29
+ );
30
+ }
31
+
32
+ function NavigationMenuList({
33
+ className,
34
+ ...props
35
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
36
+ return (
37
+ <NavigationMenuPrimitive.List
38
+ data-slot="navigation-menu-list"
39
+ className={cn(
40
+ "group flex flex-1 list-none items-center justify-center gap-1",
41
+ className,
42
+ )}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+
48
+ function NavigationMenuItem({
49
+ className,
50
+ ...props
51
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
52
+ return (
53
+ <NavigationMenuPrimitive.Item
54
+ data-slot="navigation-menu-item"
55
+ className={cn("relative", className)}
56
+ {...props}
57
+ />
58
+ );
59
+ }
60
+
61
+ const navigationMenuTriggerStyle = cva(
62
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
63
+ );
64
+
65
+ function NavigationMenuTrigger({
66
+ className,
67
+ children,
68
+ ...props
69
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
70
+ return (
71
+ <NavigationMenuPrimitive.Trigger
72
+ data-slot="navigation-menu-trigger"
73
+ className={cn(navigationMenuTriggerStyle(), "group", className)}
74
+ {...props}
75
+ >
76
+ {children}{" "}
77
+ <ChevronDownIcon
78
+ className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
79
+ aria-hidden="true"
80
+ />
81
+ </NavigationMenuPrimitive.Trigger>
82
+ );
83
+ }
84
+
85
+ function NavigationMenuContent({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
89
+ return (
90
+ <NavigationMenuPrimitive.Content
91
+ data-slot="navigation-menu-content"
92
+ className={cn(
93
+ "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
94
+ "group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
95
+ className,
96
+ )}
97
+ {...props}
98
+ />
99
+ );
100
+ }
101
+
102
+ function NavigationMenuViewport({
103
+ className,
104
+ ...props
105
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
106
+ return (
107
+ <div
108
+ className={cn(
109
+ "absolute top-full left-0 isolate z-50 flex justify-center",
110
+ )}
111
+ >
112
+ <NavigationMenuPrimitive.Viewport
113
+ data-slot="navigation-menu-viewport"
114
+ className={cn(
115
+ "origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
116
+ className,
117
+ )}
118
+ {...props}
119
+ />
120
+ </div>
121
+ );
122
+ }
123
+
124
+ function NavigationMenuLink({
125
+ className,
126
+ ...props
127
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
128
+ return (
129
+ <NavigationMenuPrimitive.Link
130
+ data-slot="navigation-menu-link"
131
+ className={cn(
132
+ "data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
133
+ className,
134
+ )}
135
+ {...props}
136
+ />
137
+ );
138
+ }
139
+
140
+ function NavigationMenuIndicator({
141
+ className,
142
+ ...props
143
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
144
+ return (
145
+ <NavigationMenuPrimitive.Indicator
146
+ data-slot="navigation-menu-indicator"
147
+ className={cn(
148
+ "data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
149
+ className,
150
+ )}
151
+ {...props}
152
+ >
153
+ <div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
154
+ </NavigationMenuPrimitive.Indicator>
155
+ );
156
+ }
157
+
158
+ export {
159
+ NavigationMenu,
160
+ NavigationMenuList,
161
+ NavigationMenuItem,
162
+ NavigationMenuContent,
163
+ NavigationMenuTrigger,
164
+ NavigationMenuLink,
165
+ NavigationMenuIndicator,
166
+ NavigationMenuViewport,
167
+ navigationMenuTriggerStyle,
168
+ };
src/app/components/ui/pagination.tsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import {
3
+ ChevronLeftIcon,
4
+ ChevronRightIcon,
5
+ MoreHorizontalIcon,
6
+ } from "lucide-react";
7
+
8
+ import { cn } from "./utils";
9
+ import { Button, buttonVariants } from "./button";
10
+
11
+ function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
12
+ return (
13
+ <nav
14
+ role="navigation"
15
+ aria-label="pagination"
16
+ data-slot="pagination"
17
+ className={cn("mx-auto flex w-full justify-center", className)}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ function PaginationContent({
24
+ className,
25
+ ...props
26
+ }: React.ComponentProps<"ul">) {
27
+ return (
28
+ <ul
29
+ data-slot="pagination-content"
30
+ className={cn("flex flex-row items-center gap-1", className)}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ function PaginationItem({ ...props }: React.ComponentProps<"li">) {
37
+ return <li data-slot="pagination-item" {...props} />;
38
+ }
39
+
40
+ type PaginationLinkProps = {
41
+ isActive?: boolean;
42
+ } & Pick<React.ComponentProps<typeof Button>, "size"> &
43
+ React.ComponentProps<"a">;
44
+
45
+ function PaginationLink({
46
+ className,
47
+ isActive,
48
+ size = "icon",
49
+ ...props
50
+ }: PaginationLinkProps) {
51
+ return (
52
+ <a
53
+ aria-current={isActive ? "page" : undefined}
54
+ data-slot="pagination-link"
55
+ data-active={isActive}
56
+ className={cn(
57
+ buttonVariants({
58
+ variant: isActive ? "outline" : "ghost",
59
+ size,
60
+ }),
61
+ className,
62
+ )}
63
+ {...props}
64
+ />
65
+ );
66
+ }
67
+
68
+ function PaginationPrevious({
69
+ className,
70
+ ...props
71
+ }: React.ComponentProps<typeof PaginationLink>) {
72
+ return (
73
+ <PaginationLink
74
+ aria-label="Go to previous page"
75
+ size="default"
76
+ className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
77
+ {...props}
78
+ >
79
+ <ChevronLeftIcon />
80
+ <span className="hidden sm:block">Previous</span>
81
+ </PaginationLink>
82
+ );
83
+ }
84
+
85
+ function PaginationNext({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<typeof PaginationLink>) {
89
+ return (
90
+ <PaginationLink
91
+ aria-label="Go to next page"
92
+ size="default"
93
+ className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
94
+ {...props}
95
+ >
96
+ <span className="hidden sm:block">Next</span>
97
+ <ChevronRightIcon />
98
+ </PaginationLink>
99
+ );
100
+ }
101
+
102
+ function PaginationEllipsis({
103
+ className,
104
+ ...props
105
+ }: React.ComponentProps<"span">) {
106
+ return (
107
+ <span
108
+ aria-hidden
109
+ data-slot="pagination-ellipsis"
110
+ className={cn("flex size-9 items-center justify-center", className)}
111
+ {...props}
112
+ >
113
+ <MoreHorizontalIcon className="size-4" />
114
+ <span className="sr-only">More pages</span>
115
+ </span>
116
+ );
117
+ }
118
+
119
+ export {
120
+ Pagination,
121
+ PaginationContent,
122
+ PaginationLink,
123
+ PaginationItem,
124
+ PaginationPrevious,
125
+ PaginationNext,
126
+ PaginationEllipsis,
127
+ };
src/app/components/ui/popover.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function Popover({
9
+ ...props
10
+ }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
11
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />;
12
+ }
13
+
14
+ function PopoverTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
17
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
18
+ }
19
+
20
+ function PopoverContent({
21
+ className,
22
+ align = "center",
23
+ sideOffset = 4,
24
+ ...props
25
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
26
+ return (
27
+ <PopoverPrimitive.Portal>
28
+ <PopoverPrimitive.Content
29
+ data-slot="popover-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-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
34
+ className,
35
+ )}
36
+ {...props}
37
+ />
38
+ </PopoverPrimitive.Portal>
39
+ );
40
+ }
41
+
42
+ function PopoverAnchor({
43
+ ...props
44
+ }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
45
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
46
+ }
47
+
48
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
src/app/components/ui/progress.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function Progress({
9
+ className,
10
+ value,
11
+ ...props
12
+ }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
13
+ return (
14
+ <ProgressPrimitive.Root
15
+ data-slot="progress"
16
+ className={cn(
17
+ "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
18
+ className,
19
+ )}
20
+ {...props}
21
+ >
22
+ <ProgressPrimitive.Indicator
23
+ data-slot="progress-indicator"
24
+ className="bg-primary h-full w-full flex-1 transition-all"
25
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
26
+ />
27
+ </ProgressPrimitive.Root>
28
+ );
29
+ }
30
+
31
+ export { Progress };