oceddyyy commited on
Commit
182e8e9
·
verified ·
1 Parent(s): 84543f9

Upload 69 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +37 -0
  2. README.md +45 -11
  3. app.py +182 -0
  4. dataset.json +0 -0
  5. index.html +15 -0
  6. nginx.conf +40 -0
  7. package-lock.json +0 -0
  8. package.json +60 -0
  9. requirements.txt +5 -0
  10. src/App.tsx +5 -0
  11. src/Attributions.md +3 -0
  12. src/components/ChatInterface.tsx +537 -0
  13. src/components/FeedbackPopup.tsx +240 -0
  14. src/components/Message.tsx +130 -0
  15. src/components/TypingIndicator.tsx +59 -0
  16. src/components/figma/ImageWithFallback.tsx +27 -0
  17. src/components/ui/accordion.tsx +66 -0
  18. src/components/ui/alert-dialog.tsx +157 -0
  19. src/components/ui/alert.tsx +66 -0
  20. src/components/ui/aspect-ratio.tsx +11 -0
  21. src/components/ui/avatar.tsx +53 -0
  22. src/components/ui/badge.tsx +46 -0
  23. src/components/ui/breadcrumb.tsx +109 -0
  24. src/components/ui/button.tsx +58 -0
  25. src/components/ui/calendar.tsx +75 -0
  26. src/components/ui/card.tsx +92 -0
  27. src/components/ui/carousel.tsx +241 -0
  28. src/components/ui/chart.tsx +353 -0
  29. src/components/ui/checkbox.tsx +32 -0
  30. src/components/ui/collapsible.tsx +33 -0
  31. src/components/ui/command.tsx +179 -0
  32. src/components/ui/context-menu.tsx +252 -0
  33. src/components/ui/dialog.tsx +135 -0
  34. src/components/ui/drawer.tsx +132 -0
  35. src/components/ui/dropdown-menu.tsx +257 -0
  36. src/components/ui/form.tsx +168 -0
  37. src/components/ui/hover-card.tsx +44 -0
  38. src/components/ui/input-otp.tsx +77 -0
  39. src/components/ui/input.tsx +21 -0
  40. src/components/ui/label.tsx +24 -0
  41. src/components/ui/menubar.tsx +276 -0
  42. src/components/ui/navigation-menu.tsx +168 -0
  43. src/components/ui/pagination.tsx +127 -0
  44. src/components/ui/popover.tsx +48 -0
  45. src/components/ui/progress.tsx +31 -0
  46. src/components/ui/radio-group.tsx +45 -0
  47. src/components/ui/resizable.tsx +56 -0
  48. src/components/ui/scroll-area.tsx +58 -0
  49. src/components/ui/select.tsx +189 -0
  50. src/components/ui/separator.tsx +28 -0
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build UI
2
+ FROM node:20 AS ui-builder
3
+ WORKDIR /app
4
+ COPY package.json package-lock.json* ./
5
+ RUN npm install
6
+ COPY . .
7
+ RUN npm run build
8
+
9
+ # Build AI
10
+ FROM python:3.10-slim AS ai-builder
11
+ WORKDIR /app
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+ COPY app.py dataset.json ./
15
+
16
+ # Final image
17
+ FROM nginx:alpine
18
+
19
+ # Copy UI build
20
+ COPY --from=ui-builder /app/dist /usr/share/nginx/html
21
+
22
+ # Copy AI app
23
+ COPY --from=ai-builder /app/app.py /app/dataset.json /app/
24
+ COPY requirements.txt /app/
25
+
26
+ # Custom Nginx config for reverse proxy
27
+ COPY nginx.conf /etc/nginx/nginx.conf
28
+
29
+ # Install Python for backend
30
+ RUN apk add --no-cache python3 py3-pip
31
+ RUN pip3 install --no-cache-dir -r /app/requirements.txt
32
+
33
+ # Expose port
34
+ EXPOSE 7860
35
+
36
+ # Start both Nginx and Flask
37
+ CMD python3 /app/app.py & nginx -g 'daemon off;'
README.md CHANGED
@@ -1,11 +1,45 @@
1
- ---
2
- title: UnivAi
3
- emoji: 👀
4
- colorFrom: purple
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # UnivAI Chatbot Interface & AI Backend
2
+
3
+ This project merges a React UI (Vite) and a Flask AI backend into a single Hugging Face Space using Docker.
4
+
5
+ ## Features
6
+
7
+ - Conversational UI for university handbook queries
8
+ - AI backend (Sentence Transformers + LLM) for semantic search and natural responses
9
+ - Feedback API for upvote/downvote tuning
10
+
11
+ ## Running Locally
12
+
13
+ ```bash
14
+ docker build -t univai-chatbot .
15
+ docker run -p 7860:7860 univai-chatbot
16
+ ```
17
+
18
+ - UI available at `http://localhost:7860`
19
+ - API available at `http://localhost:7860/api/chat` and `/api/feedback`
20
+
21
+ ## Endpoints
22
+
23
+ - **POST /api/chat**
24
+ `{ "query": "your question", "dev_mode": false }` → `{ "response": "answer" }`
25
+ - **POST /api/feedback**
26
+ `{ "query": "...", "response": "...", "feedback": "positive"|"negative" }` → `{ "status": "success" }`
27
+
28
+ ## Hugging Face Spaces
29
+
30
+ - Push this repo and select Docker SDK.
31
+ - The UI and AI will run together, with Nginx proxying `/api/` to the backend.
32
+
33
+ ## File Structure
34
+
35
+ - `src/` — React UI
36
+ - `app.py` — Flask AI backend
37
+ - `Dockerfile` — unified build
38
+ - `nginx.conf` — reverse proxy config
39
+ - `requirements.txt` — Python dependencies
40
+ - `dataset.json` — university Q&A set
41
+
42
+ ## Connecting UI to AI
43
+
44
+ - The UI should send requests to `/api/chat` and `/api/feedback`.
45
+ - The UI should send requests to `/api/chat` and `/api/feedback`.
app.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ os.environ["HF_HOME"] = "/tmp/.cache"
3
+ os.environ["TRANSFORMERS_CACHE"] = "/tmp/.cache"
4
+ os.environ["HF_DATASETS_CACHE"] = "/tmp/.cache"
5
+ os.environ["SENTENCE_TRANSFORMERS_HOME"] = "/tmp/.cache"
6
+ os.makedirs("/tmp/.cache", exist_ok=True)
7
+
8
+ import json
9
+ from sentence_transformers import SentenceTransformer
10
+ from sklearn.metrics.pairwise import cosine_similarity
11
+ import numpy as np
12
+ import os
13
+ from huggingface_hub import upload_file, hf_hub_download, InferenceClient
14
+ from flask import Flask, request, jsonify
15
+
16
+ embedding_model = SentenceTransformer('paraphrase-mpnet-base-v2')
17
+ inference_token = os.getenv("HF_TOKEN") or os.getenv("PUP_AI_Chatbot_Token")
18
+ inference_client = InferenceClient(
19
+ model="mistralai/Mixtral-8x7B-Instruct-v0.1",
20
+ token=inference_token
21
+ )
22
+
23
+ with open("dataset.json", "r") as f:
24
+ dataset = json.load(f)
25
+
26
+ questions = [item["question"] for item in dataset]
27
+ answers = [item["answer"] for item in dataset]
28
+ question_embeddings = embedding_model.encode(questions, convert_to_tensor=True)
29
+
30
+ chat_history = []
31
+ feedback_data = []
32
+ feedback_questions = []
33
+ feedback_embeddings = None
34
+ dev_mode = {"enabled": False}
35
+
36
+ feedback_path = "/tmp/outputs/feedback.json"
37
+ os.makedirs("/tmp/outputs", exist_ok=True)
38
+
39
+ try:
40
+ hf_token = os.getenv("PUP_AI_Chatbot_Token")
41
+ downloaded_path = hf_hub_download(
42
+ repo_id="oceddyyy/University_Inquiries_Feedback",
43
+ filename="feedback.json",
44
+ repo_type="dataset",
45
+ token=hf_token
46
+ )
47
+ with open(downloaded_path, "r") as f:
48
+ feedback_data = json.load(f)
49
+ feedback_questions = [item["question"] for item in feedback_data]
50
+ if feedback_questions:
51
+ feedback_embeddings = embedding_model.encode(feedback_questions, convert_to_tensor=True)
52
+
53
+ with open(feedback_path, "w") as f_local:
54
+ json.dump(feedback_data, f_local, indent=4)
55
+
56
+ except Exception as e:
57
+ print(f"[Startup] Feedback not loaded from Hugging Face. Using local only. Reason: {e}")
58
+ feedback_data = []
59
+
60
+ def upload_feedback_to_hf():
61
+ hf_token = os.getenv("PUP_AI_Chatbot_Token")
62
+ if not hf_token:
63
+ raise ValueError("Hugging Face token not found in environment variables!")
64
+
65
+ try:
66
+ upload_file(
67
+ path_or_fileobj=feedback_path,
68
+ path_in_repo="feedback.json",
69
+ repo_id="oceddyyy/University_Inquiries_Feedback",
70
+ repo_type="dataset",
71
+ token=hf_token
72
+ )
73
+ print("Feedback uploaded to Hugging Face successfully.")
74
+ except Exception as e:
75
+ print(f"Error uploading feedback to HF: {e}")
76
+
77
+ def chatbot_response(query):
78
+ query_embedding = embedding_model.encode([query], convert_to_tensor=True)
79
+
80
+ if feedback_embeddings is not None:
81
+ feedback_scores = cosine_similarity(query_embedding.cpu().numpy(), feedback_embeddings.cpu().numpy())[0]
82
+ best_idx = int(np.argmax(feedback_scores))
83
+ best_score = feedback_scores[best_idx]
84
+ matched_feedback = feedback_data[best_idx]
85
+
86
+ base_threshold = 0.8
87
+ upvotes = matched_feedback.get("upvotes", 0)
88
+ downvotes = matched_feedback.get("downvotes", 0)
89
+ adjusted_threshold = base_threshold - (0.01 * upvotes) + (0.01 * downvotes)
90
+ dynamic_threshold = min(max(adjusted_threshold, 0.4), 1.0)
91
+
92
+ if best_score >= dynamic_threshold:
93
+ response = matched_feedback["response"]
94
+ return response
95
+
96
+ similarity_scores = cosine_similarity(query_embedding.cpu().numpy(), question_embeddings.cpu().numpy())[0]
97
+ best_idx = int(np.argmax(similarity_scores))
98
+ best_score = similarity_scores[best_idx]
99
+ matched_item = dataset[best_idx]
100
+ matched_a = matched_item.get("answer", "")
101
+
102
+ if best_score < 0.4:
103
+ response = "Sorry, but the PUP handbook does not contain such information."
104
+ else:
105
+ if dev_mode["enabled"]:
106
+ prompt = (
107
+ f"A student asked:\n\"{query}\"\n\n"
108
+ f"Relevant handbook info:\n\"{matched_a}\"\n\n"
109
+ f"Please answer based only on this handbook content."
110
+ )
111
+ try:
112
+ response = inference_client.text_generation(prompt, max_new_tokens=200, temperature=0.7)
113
+ except Exception as e:
114
+ print(f"[ERROR] HF inference failed: {e}")
115
+ response = f"(Fallback) {matched_a}"
116
+ else:
117
+ if "month" in matched_item and "year" in matched_item:
118
+ response = f"As of {matched_item['month']}, {matched_item['year']}, {matched_a}"
119
+ else:
120
+ response = f"According to 2019 Proposed PUP Handbook, {matched_a}"
121
+
122
+ return response.strip()
123
+
124
+ def record_feedback(feedback, query, response):
125
+ global feedback_embeddings, feedback_questions
126
+ matched = False
127
+ new_embedding = embedding_model.encode([query], convert_to_tensor=True)
128
+
129
+ for item in feedback_data:
130
+ existing_embedding = embedding_model.encode([item["question"]], convert_to_tensor=True)
131
+ similarity = cosine_similarity(existing_embedding.cpu().numpy(), new_embedding.cpu().numpy())[0][0]
132
+ if similarity >= 0.8 and item["response"] == response:
133
+ matched = True
134
+ votes = {"positive": "upvotes", "negative": "downvotes"}
135
+ item[votes[feedback]] = item.get(votes[feedback], 0) + 1
136
+ break
137
+
138
+ if not matched:
139
+ entry = {
140
+ "question": query,
141
+ "response": response,
142
+ "feedback": feedback,
143
+ "upvotes": 1 if feedback == "positive" else 0,
144
+ "downvotes": 1 if feedback == "negative" else 0
145
+ }
146
+ feedback_data.append(entry)
147
+
148
+ with open(feedback_path, "w") as f:
149
+ json.dump(feedback_data, f, indent=4)
150
+
151
+ feedback_questions = [item["question"] for item in feedback_data]
152
+ if feedback_questions:
153
+ feedback_embeddings = embedding_model.encode(feedback_questions, convert_to_tensor=True)
154
+
155
+ upload_feedback_to_hf()
156
+
157
+ app = Flask(__name__)
158
+
159
+ @app.route("/chat", methods=["POST"])
160
+ def chat():
161
+ data = request.json
162
+ query = data.get("query", "")
163
+ dev = data.get("dev_mode", False)
164
+ dev_mode["enabled"] = dev
165
+ response = chatbot_response(query)
166
+ return jsonify({"response": response})
167
+
168
+ @app.route("/feedback", methods=["POST"])
169
+ def feedback():
170
+ data = request.json
171
+ query = data.get("query", "")
172
+ response = data.get("response", "")
173
+ feedback_type = data.get("feedback", "")
174
+ record_feedback(feedback_type, query, response)
175
+ return jsonify({"status": "success"})
176
+
177
+ @app.route("/", methods=["GET"])
178
+ def index():
179
+ return "University Inquiries AI Chatbot API. Use POST /chat or /feedback.", 200
180
+
181
+ if __name__ == "__main__":
182
+ app.run(host="0.0.0.0", port=7861)
dataset.json ADDED
The diff for this file is too large to render. See raw diff
 
index.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <title>UnivAI Chatbot Interface</title>
8
+ </head>
9
+
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.tsx"></script>
13
+ </body>
14
+ </html>
15
+
nginx.conf ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ worker_processes 1;
2
+
3
+ pid /tmp/nginx.pid;
4
+
5
+ events { worker_connections 1024; }
6
+
7
+ http {
8
+ include mime.types;
9
+ default_type application/octet-stream;
10
+
11
+ sendfile on;
12
+ keepalive_timeout 65;
13
+
14
+ # Use /tmp for temp files to avoid permission issues
15
+ client_body_temp_path /tmp/client_temp;
16
+ proxy_temp_path /tmp/proxy_temp;
17
+ fastcgi_temp_path /tmp/fastcgi_temp;
18
+ uwsgi_temp_path /tmp/uwsgi_temp;
19
+ scgi_temp_path /tmp/scgi_temp;
20
+
21
+ server {
22
+ listen 7860;
23
+ server_name localhost;
24
+
25
+ root /usr/share/nginx/html;
26
+ index index.html index.htm;
27
+
28
+ location /api/ {
29
+ proxy_pass http://127.0.0.1:7861;
30
+ proxy_set_header Host $host;
31
+ proxy_set_header X-Real-IP $remote_addr;
32
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
33
+ proxy_set_header X-Forwarded-Proto $scheme;
34
+ }
35
+
36
+ location / {
37
+ try_files $uri $uri/ /index.html;
38
+ }
39
+ }
40
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ {
3
+ "name": "UnivAI Chatbot Interface",
4
+ "version": "0.1.0",
5
+ "private": true,
6
+ "dependencies": {
7
+ "@radix-ui/react-accordion": "^1.2.3",
8
+ "@radix-ui/react-alert-dialog": "^1.1.6",
9
+ "@radix-ui/react-aspect-ratio": "^1.1.2",
10
+ "@radix-ui/react-avatar": "^1.1.3",
11
+ "@radix-ui/react-checkbox": "^1.1.4",
12
+ "@radix-ui/react-collapsible": "^1.1.3",
13
+ "@radix-ui/react-context-menu": "^2.2.6",
14
+ "@radix-ui/react-dialog": "^1.1.6",
15
+ "@radix-ui/react-dropdown-menu": "^2.1.6",
16
+ "@radix-ui/react-hover-card": "^1.1.6",
17
+ "@radix-ui/react-label": "^2.1.2",
18
+ "@radix-ui/react-menubar": "^1.1.6",
19
+ "@radix-ui/react-navigation-menu": "^1.2.5",
20
+ "@radix-ui/react-popover": "^1.1.6",
21
+ "@radix-ui/react-progress": "^1.1.2",
22
+ "@radix-ui/react-radio-group": "^1.2.3",
23
+ "@radix-ui/react-scroll-area": "^1.2.3",
24
+ "@radix-ui/react-select": "^2.1.6",
25
+ "@radix-ui/react-separator": "^1.1.2",
26
+ "@radix-ui/react-slider": "^1.2.3",
27
+ "@radix-ui/react-slot": "^1.1.2",
28
+ "@radix-ui/react-switch": "^1.1.3",
29
+ "@radix-ui/react-tabs": "^1.1.3",
30
+ "@radix-ui/react-toggle": "^1.1.2",
31
+ "@radix-ui/react-toggle-group": "^1.1.2",
32
+ "@radix-ui/react-tooltip": "^1.1.8",
33
+ "class-variance-authority": "^0.7.1",
34
+ "clsx": "*",
35
+ "cmdk": "^1.1.1",
36
+ "embla-carousel-react": "^8.6.0",
37
+ "input-otp": "^1.4.2",
38
+ "lucide-react": "^0.487.0",
39
+ "motion": "*",
40
+ "next-themes": "^0.4.6",
41
+ "react": "^18.3.1",
42
+ "react-day-picker": "^8.10.1",
43
+ "react-dom": "^18.3.1",
44
+ "react-hook-form": "^7.55.0",
45
+ "react-resizable-panels": "^2.1.7",
46
+ "recharts": "^2.15.2",
47
+ "sonner": "^2.0.3",
48
+ "tailwind-merge": "*",
49
+ "vaul": "^1.1.2"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^20.10.0",
53
+ "@vitejs/plugin-react-swc": "^3.10.2",
54
+ "vite": "^5.4.19"
55
+ },
56
+ "scripts": {
57
+ "dev": "vite",
58
+ "build": "vite build"
59
+ }
60
+ }
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ transformers
2
+ sentence-transformers
3
+ scikit-learn
4
+ numpy
5
+ flask
src/App.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { ChatInterface } from "./components/ChatInterface";
2
+
3
+ export default function App() {
4
+ return <ChatInterface />;
5
+ }
src/Attributions.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
2
+
3
+ This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).
src/components/ChatInterface.tsx ADDED
@@ -0,0 +1,537 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+ import { useState, useRef, useEffect } from 'react';
3
+ import { Send, MessageCircle, Settings, Search, BarChart3, User, Bot, Sparkles, HelpCircle, Moon, Sun, Globe, Database, BookOpen, Crown, Zap } from 'lucide-react';
4
+ import { Button } from './ui/button';
5
+ import { Input } from './ui/input';
6
+ import { Message } from './Message';
7
+ import { TypingIndicator } from './TypingIndicator';
8
+ import { motion, AnimatePresence } from 'motion/react';
9
+
10
+ interface ChatMessage {
11
+ id: string;
12
+ content: string;
13
+ isUser: boolean;
14
+ timestamp: Date;
15
+ isPlusResponse?: boolean;
16
+ }
17
+
18
+ export function ChatInterface() {
19
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
20
+ const [inputValue, setInputValue] = useState('');
21
+ const [isTyping, setIsTyping] = useState(false);
22
+ const [showComingSoon, setShowComingSoon] = useState(false);
23
+ const [isDarkMode, setIsDarkMode] = useState(false);
24
+ const [isUnivAiPlusMode, setIsUnivAiPlusMode] = useState(false);
25
+ const [hasUsedPlusResponse, setHasUsedPlusResponse] = useState(false);
26
+ const messagesEndRef = useRef<HTMLDivElement>(null);
27
+
28
+ // Initialize messages based on current mode
29
+ useEffect(() => {
30
+ const initialMessage: ChatMessage = {
31
+ id: '1',
32
+ content: isUnivAiPlusMode
33
+ ? "Feel free to use UnivAi+++ for better conversation, however, the number of responses is limited to 1 per conversation as the AI has only limited computational resources."
34
+ : "Hello! I'm your AI assistant. How can I help you today? Feel free to ask me anything!",
35
+ isUser: false,
36
+ timestamp: new Date(),
37
+ isPlusResponse: isUnivAiPlusMode
38
+ };
39
+ setMessages([initialMessage]);
40
+ setHasUsedPlusResponse(false);
41
+ }, [isUnivAiPlusMode]);
42
+
43
+ const scrollToBottom = () => {
44
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
45
+ };
46
+
47
+ useEffect(() => {
48
+ scrollToBottom();
49
+ }, [messages, isTyping]);
50
+
51
+ useEffect(() => {
52
+ // Apply dark mode class to document
53
+ if (isDarkMode) {
54
+ document.documentElement.classList.add('dark');
55
+ } else {
56
+ document.documentElement.classList.remove('dark');
57
+ }
58
+ }, [isDarkMode]);
59
+
60
+ const handleSendMessage = async (e: React.FormEvent) => {
61
+ e.preventDefault();
62
+ if (!inputValue.trim()) return;
63
+ if (isUnivAiPlusMode && hasUsedPlusResponse) return;
64
+
65
+ const userMessage: ChatMessage = {
66
+ id: Date.now().toString(),
67
+ content: inputValue,
68
+ isUser: true,
69
+ timestamp: new Date(),
70
+ };
71
+
72
+ setMessages(prev => [...prev, userMessage]);
73
+ setInputValue('');
74
+ setIsTyping(true);
75
+
76
+ // Call AI backend
77
+ fetch('/api/chat', {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify({ query: inputValue, dev_mode: isUnivAiPlusMode }),
81
+ })
82
+ .then(res => res.json())
83
+ .then(data => {
84
+ const botResponse: ChatMessage = {
85
+ id: (Date.now() + 1).toString(),
86
+ content: data.response,
87
+ isUser: false,
88
+ timestamp: new Date(),
89
+ isPlusResponse: isUnivAiPlusMode,
90
+ };
91
+ setMessages(prev => [...prev, botResponse]);
92
+ setIsTyping(false);
93
+ if (isUnivAiPlusMode) setHasUsedPlusResponse(true);
94
+ })
95
+ .catch(() => {
96
+ setIsTyping(false);
97
+ });
98
+ };
99
+
100
+ const quickActions = [
101
+ { label: 'Tell me a joke', icon: <Sparkles size={16} /> },
102
+ { label: 'Explain quantum physics', icon: <BarChart3 size={16} /> },
103
+ { label: 'What can you help me with?', icon: <MessageCircle size={16} /> }
104
+ ];
105
+
106
+ const handleQuickAction = (action: string) => {
107
+ if (isUnivAiPlusMode && hasUsedPlusResponse) return; // Prevent quick actions if already used Plus response
108
+ setInputValue(action);
109
+ };
110
+
111
+ const handleComingSoonClick = () => {
112
+ setShowComingSoon(true);
113
+ setTimeout(() => {
114
+ setShowComingSoon(false);
115
+ }, 2000);
116
+ };
117
+
118
+ const toggleDarkMode = () => {
119
+ setIsDarkMode(!isDarkMode);
120
+ };
121
+
122
+ const toggleUnivAiMode = () => {
123
+ setIsUnivAiPlusMode(!isUnivAiPlusMode);
124
+ };
125
+
126
+ const sidebarItems = [
127
+ { icon: <MessageCircle size={20} />, active: true },
128
+ { icon: <BarChart3 size={20} />, active: false },
129
+ { icon: <Search size={20} />, active: false },
130
+ { icon: <User size={20} />, active: false },
131
+ { icon: <Settings size={20} />, active: false },
132
+ ];
133
+
134
+ const isInputDisabled = isTyping || (isUnivAiPlusMode && hasUsedPlusResponse);
135
+
136
+ return (
137
+ <div className={`h-screen flex p-2 transition-all duration-500 ${
138
+ isUnivAiPlusMode
139
+ ? 'bg-gradient-to-br from-purple-100 via-pink-50 to-amber-50 dark:from-purple-900 dark:via-indigo-900 dark:to-amber-900'
140
+ : 'bg-gradient-to-br from-orange-100 via-red-50 to-yellow-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900'
141
+ }`}>
142
+ {/* Coming Soon Toast */}
143
+ <AnimatePresence>
144
+ {showComingSoon && (
145
+ <motion.div
146
+ initial={{ opacity: 0, y: -50, x: '-50%' }}
147
+ animate={{ opacity: 1, y: 0, x: '-50%' }}
148
+ exit={{ opacity: 0, y: -50, x: '-50%' }}
149
+ className="fixed top-4 left-1/2 z-50 bg-gradient-to-r from-red-500 to-orange-500 text-white px-6 py-3 rounded-lg shadow-lg shadow-red-500/30"
150
+ >
151
+ <p className="text-sm">Coming soon!</p>
152
+ </motion.div>
153
+ )}
154
+ </AnimatePresence>
155
+
156
+ {/* Usage Limit Warning */}
157
+ <AnimatePresence>
158
+ {isUnivAiPlusMode && hasUsedPlusResponse && (
159
+ <motion.div
160
+ initial={{ opacity: 0, y: -50, x: '-50%' }}
161
+ animate={{ opacity: 1, y: 0, x: '-50%' }}
162
+ exit={{ opacity: 0, y: -50, x: '-50%' }}
163
+ className="fixed top-16 left-1/2 z-50 bg-gradient-to-r from-purple-500 to-pink-500 text-white px-6 py-3 rounded-lg shadow-lg shadow-purple-500/30"
164
+ >
165
+ <p className="text-sm">UnivAi+++ limit reached. Switch to UnivAi to continue.</p>
166
+ </motion.div>
167
+ )}
168
+ </AnimatePresence>
169
+
170
+ {/* Dark Mode Toggle - Top Right */}
171
+ <motion.button
172
+ initial={{ opacity: 0, scale: 0.8 }}
173
+ animate={{ opacity: 1, scale: 1 }}
174
+ onClick={toggleDarkMode}
175
+ className={`fixed top-3 right-3 z-40 w-10 h-10 rounded-full text-white flex items-center justify-center shadow-lg transition-all duration-200 hover:scale-110 ${
176
+ isUnivAiPlusMode
177
+ ? 'bg-gradient-to-r from-purple-500 to-pink-500 shadow-purple-500/30 hover:shadow-purple-500/50'
178
+ : 'bg-gradient-to-r from-red-500 to-orange-500 shadow-red-500/30 hover:shadow-red-500/50'
179
+ }`}
180
+ >
181
+ <motion.div
182
+ key={isDarkMode ? 'dark' : 'light'}
183
+ initial={{ rotate: -180, opacity: 0 }}
184
+ animate={{ rotate: 0, opacity: 1 }}
185
+ transition={{ duration: 0.3 }}
186
+ >
187
+ {isDarkMode ? <Sun size={18} /> : <Moon size={18} />}
188
+ </motion.div>
189
+ </motion.button>
190
+
191
+ {/* Sidebar */}
192
+ <motion.div
193
+ initial={{ x: -20, opacity: 0 }}
194
+ animate={{ x: 0, opacity: 1 }}
195
+ className="w-14 flex flex-col items-center py-3 px-1 mr-2"
196
+ >
197
+ {/* Logo */}
198
+ <div className={`w-9 h-9 rounded-lg flex items-center justify-center mb-6 shadow-lg transition-all duration-300 ${
199
+ isUnivAiPlusMode
200
+ ? 'bg-gradient-to-r from-purple-500 to-pink-500 shadow-purple-500/30'
201
+ : 'bg-gradient-to-r from-red-500 to-orange-500 shadow-red-500/30'
202
+ }`}>
203
+ {isUnivAiPlusMode ? <Crown className="text-white" size={20} /> : <Bot className="text-white" size={20} />}
204
+ </div>
205
+
206
+ {/* Navigation */}
207
+ <div className="flex flex-col gap-3">
208
+ {sidebarItems.map((item, index) => (
209
+ <motion.button
210
+ key={index}
211
+ whileHover={{ scale: 1.1 }}
212
+ whileTap={{ scale: 0.95 }}
213
+ onClick={!item.active ? handleComingSoonClick : undefined}
214
+ className={`
215
+ w-9 h-9 rounded-lg flex items-center justify-center transition-all duration-200
216
+ ${item.active
217
+ ? isUnivAiPlusMode
218
+ ? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg shadow-purple-500/30'
219
+ : 'bg-gradient-to-r from-red-500 to-orange-500 text-white shadow-lg shadow-red-500/30'
220
+ : 'bg-white/80 text-red-600 hover:bg-white hover:text-red-700 shadow-sm cursor-pointer dark:bg-gray-800/80 dark:text-red-400 dark:hover:bg-gray-700'
221
+ }
222
+ `}
223
+ >
224
+ {item.active ? item.icon : <HelpCircle size={18} />}
225
+ </motion.button>
226
+ ))}
227
+ </div>
228
+ </motion.div>
229
+
230
+ {/* Main Container */}
231
+ <div className={`flex-1 flex flex-col rounded-2xl backdrop-blur-sm border overflow-hidden shadow-xl transition-all duration-300 ${
232
+ isUnivAiPlusMode
233
+ ? 'bg-white/95 border-purple-200/50 shadow-purple-100/50 dark:bg-gray-900/95 dark:border-purple-700/50 dark:shadow-purple-900/50'
234
+ : 'bg-white/90 border-red-200/50 shadow-red-100/50 dark:bg-gray-900/90 dark:border-gray-700/50 dark:shadow-gray-900/50'
235
+ }`}>
236
+ {/* Header */}
237
+ <motion.div
238
+ initial={{ y: -20, opacity: 0 }}
239
+ animate={{ y: 0, opacity: 1 }}
240
+ className={`p-4 border-b backdrop-blur-sm transition-all duration-300 flex-shrink-0 ${
241
+ isUnivAiPlusMode
242
+ ? 'border-purple-200/50 bg-gradient-to-r from-white/95 to-purple-50/95 dark:from-gray-900/95 dark:to-purple-900/95 dark:border-purple-700/50'
243
+ : 'border-red-200/50 bg-gradient-to-r from-white/95 to-red-50/95 dark:from-gray-900/95 dark:to-gray-800/95 dark:border-gray-700/50'
244
+ }`}
245
+ >
246
+ <div className="flex items-center justify-between">
247
+ <div className="flex items-center gap-3">
248
+ <div className="flex items-center gap-3">
249
+ <div className={`w-10 h-10 rounded-full flex items-center justify-center shadow-lg transition-all duration-300 ${
250
+ isUnivAiPlusMode
251
+ ? 'bg-gradient-to-r from-purple-400 to-pink-500 shadow-purple-500/30'
252
+ : 'bg-gradient-to-r from-yellow-400 to-orange-500 shadow-yellow-500/30'
253
+ }`}>
254
+ {isUnivAiPlusMode ? <Crown className="text-white" size={20} /> : <Bot className="text-white" size={20} />}
255
+ </div>
256
+ <div className="min-w-0 flex-1">
257
+ <div className="overflow-hidden">
258
+ <motion.h1
259
+ key={isUnivAiPlusMode ? 'plus-header' : 'regular-header'}
260
+ initial={{ opacity: 0 }}
261
+ animate={{ opacity: 1 }}
262
+ transition={{ duration: 0.2 }}
263
+ className={`transition-all duration-300 truncate whitespace-nowrap ${
264
+ isUnivAiPlusMode
265
+ ? 'text-purple-900 dark:text-purple-100'
266
+ : 'text-red-900 dark:text-red-100'
267
+ }`}
268
+ >
269
+ {isUnivAiPlusMode ? 'UnivAi+++' : 'UnivAi'}
270
+ {isUnivAiPlusMode && <Crown className="inline ml-2" size={14} />}
271
+ </motion.h1>
272
+ </div>
273
+ <p className={`text-sm transition-all duration-300 truncate ${
274
+ isUnivAiPlusMode
275
+ ? 'text-purple-700/80 dark:text-purple-300/80'
276
+ : 'text-red-700/80 dark:text-red-300/80'
277
+ }`}>
278
+ {isUnivAiPlusMode ? 'Premium AI Experience' : 'Always ready to help'}
279
+ </p>
280
+ </div>
281
+ </div>
282
+ </div>
283
+ <div className="flex items-center gap-2">
284
+ <div className={`w-6 h-6 rounded-full shadow-lg transition-all duration-300 ${
285
+ isUnivAiPlusMode
286
+ ? 'bg-gradient-to-r from-purple-400 to-pink-500 shadow-purple-500/20'
287
+ : 'bg-gradient-to-r from-yellow-400 to-orange-500 shadow-yellow-500/20'
288
+ }`}></div>
289
+ </div>
290
+ </div>
291
+ </motion.div>
292
+
293
+ {/* Chat Container */}
294
+ <div className="flex-1 flex min-h-0">
295
+ {/* Messages Area */}
296
+ <div className="flex-1 flex flex-col min-h-0">
297
+ {/* Messages */}
298
+ <div className={`flex-1 overflow-y-auto p-4 transition-all duration-300 ${
299
+ isUnivAiPlusMode
300
+ ? 'bg-gradient-to-br from-white to-purple-50/30 dark:from-gray-900 dark:to-purple-900/30'
301
+ : 'bg-gradient-to-br from-white to-red-50/30 dark:from-gray-900 dark:to-gray-800/30'
302
+ }`}>
303
+ <div className="max-w-4xl mx-auto space-y-4">
304
+ {messages.map((message) => (
305
+ <Message
306
+ content={message.content}
307
+ isUser={message.isUser}
308
+ timestamp={message.timestamp}
309
+ isPlusResponse={message.isPlusResponse}
310
+ isUnivAiPlusMode={isUnivAiPlusMode}
311
+ />
312
+ ))}
313
+ {isTyping && <TypingIndicator isUnivAiPlusMode={isUnivAiPlusMode} />}
314
+ <div ref={messagesEndRef} />
315
+ </div>
316
+ </div>
317
+
318
+ {/* Quick Actions */}
319
+ {messages.length === 1 && (
320
+ <motion.div
321
+ initial={{ opacity: 0, y: 20 }}
322
+ animate={{ opacity: 1, y: 0 }}
323
+ className={`px-4 pb-3 transition-all duration-300 flex-shrink-0 ${
324
+ isUnivAiPlusMode
325
+ ? 'bg-gradient-to-r from-white to-purple-50/30 dark:from-gray-900 dark:to-purple-900/30'
326
+ : 'bg-gradient-to-r from-white to-red-50/30 dark:from-gray-900 dark:to-gray-800/30'
327
+ }`}
328
+ >
329
+ <div className="max-w-4xl mx-auto">
330
+ <p className={`text-sm mb-2 transition-all duration-300 ${
331
+ isUnivAiPlusMode
332
+ ? 'text-purple-700/80 dark:text-purple-300/80'
333
+ : 'text-red-700/80 dark:text-red-300/80'
334
+ }`}>
335
+ Try asking me about:
336
+ </p>
337
+ <div className="flex flex-wrap gap-2">
338
+ {quickActions.map((action, index) => (
339
+ <Button
340
+ key={index}
341
+ variant="outline"
342
+ size="sm"
343
+ onClick={() => handleQuickAction(action.label)}
344
+ disabled={isInputDisabled}
345
+ className={`backdrop-blur-sm transition-all duration-200 shadow-sm ${
346
+ isUnivAiPlusMode
347
+ ? 'bg-white/80 border-purple-200 text-purple-700 hover:bg-purple-50 hover:border-purple-300 disabled:opacity-50 dark:bg-gray-800/80 dark:border-purple-600 dark:text-purple-300 dark:hover:bg-purple-900/30'
348
+ : 'bg-white/80 border-red-200 text-red-700 hover:bg-red-50 hover:border-red-300 disabled:opacity-50 dark:bg-gray-800/80 dark:border-gray-600 dark:text-red-300 dark:hover:bg-gray-700'
349
+ }`}
350
+ >
351
+ {action.icon}
352
+ {action.label}
353
+ </Button>
354
+ ))}
355
+ </div>
356
+ </div>
357
+ </motion.div>
358
+ )}
359
+
360
+ {/* Input */}
361
+ <motion.div
362
+ initial={{ y: 20, opacity: 0 }}
363
+ animate={{ y: 0, opacity: 1 }}
364
+ className={`p-4 border-t backdrop-blur-sm transition-all duration-300 flex-shrink-0 ${
365
+ isUnivAiPlusMode
366
+ ? 'border-purple-200/50 bg-gradient-to-r from-white/95 to-purple-50/95 dark:from-gray-900/95 dark:to-purple-900/95 dark:border-purple-700/50'
367
+ : 'border-red-200/50 bg-gradient-to-r from-white/95 to-red-50/95 dark:from-gray-900/95 dark:to-gray-800/95 dark:border-gray-700/50'
368
+ }`}
369
+ >
370
+ <div className="max-w-4xl mx-auto">
371
+ <form onSubmit={handleSendMessage} className="flex gap-3">
372
+ <div className="flex-1 relative">
373
+ <Input
374
+ value={inputValue}
375
+ onChange={(e) => setInputValue(e.target.value)}
376
+ placeholder={
377
+ isUnivAiPlusMode && hasUsedPlusResponse
378
+ ? "Switch to UnivAi to continue..."
379
+ : "Type your message..."
380
+ }
381
+ className={`backdrop-blur-sm transition-all duration-200 shadow-sm ${
382
+ isUnivAiPlusMode
383
+ ? 'bg-white/90 border-purple-200 text-purple-900 placeholder:text-purple-500/60 focus:bg-white focus:border-purple-400 dark:bg-gray-800/90 dark:border-purple-600 dark:text-purple-100 dark:placeholder:text-purple-400/60 dark:focus:bg-gray-800'
384
+ : 'bg-white/90 border-red-200 text-red-900 placeholder:text-red-500/60 focus:bg-white focus:border-red-400 dark:bg-gray-800/90 dark:border-gray-600 dark:text-red-100 dark:placeholder:text-red-400/60 dark:focus:bg-gray-800'
385
+ }`}
386
+ disabled={isInputDisabled}
387
+ />
388
+ </div>
389
+ <Button
390
+ type="submit"
391
+ disabled={!inputValue.trim() || isInputDisabled}
392
+ className={`text-white border-0 shadow-lg transition-all duration-300 ${
393
+ isUnivAiPlusMode
394
+ ? 'bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 shadow-purple-500/30'
395
+ : 'bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 shadow-red-500/30'
396
+ }`}
397
+ >
398
+ <Send size={18} />
399
+ </Button>
400
+ </form>
401
+ </div>
402
+ </motion.div>
403
+ </div>
404
+
405
+ {/* Stats Sidebar - More compact */}
406
+ <motion.div
407
+ initial={{ x: 20, opacity: 0 }}
408
+ animate={{ x: 0, opacity: 1 }}
409
+ className={`w-72 flex-shrink-0 border-l p-4 space-y-4 transition-all duration-300 overflow-y-auto ${
410
+ isUnivAiPlusMode
411
+ ? 'border-purple-200/50 bg-gradient-to-br from-white/50 to-purple-50/50 dark:from-gray-900/50 dark:to-purple-900/50 dark:border-purple-700/50'
412
+ : 'border-red-200/50 bg-gradient-to-br from-white/50 to-yellow-50/50 dark:from-gray-900/50 dark:to-gray-800/50 dark:border-gray-700/50'
413
+ }`}
414
+ >
415
+ {/* AI Status Card - More compact */}
416
+ <div className={`rounded-xl p-4 border backdrop-blur-sm shadow-lg transition-all duration-300 min-h-[200px] ${
417
+ isUnivAiPlusMode
418
+ ? 'bg-gradient-to-br from-white/90 to-purple-50/90 border-purple-200/50 shadow-purple-100/30 dark:from-gray-800/90 dark:to-purple-900/90 dark:border-purple-600/50 dark:shadow-purple-900/30'
419
+ : 'bg-gradient-to-br from-white/90 to-red-50/90 border-red-200/50 shadow-red-100/30 dark:from-gray-800/90 dark:to-gray-700/90 dark:border-gray-600/50 dark:shadow-gray-900/30'
420
+ }`}>
421
+ <div className="flex items-center justify-center mb-3">
422
+ <div className={`w-16 h-16 rounded-full flex items-center justify-center shadow-lg transition-all duration-300 ${
423
+ isUnivAiPlusMode
424
+ ? 'bg-gradient-to-r from-purple-400 to-pink-500 shadow-purple-500/30'
425
+ : 'bg-gradient-to-r from-yellow-400 to-orange-500 shadow-yellow-500/30'
426
+ }`}>
427
+ {isUnivAiPlusMode ? <Crown className="text-white" size={24} /> : <Bot className="text-white" size={24} />}
428
+ </div>
429
+ </div>
430
+ <div className="text-center">
431
+ {/* Fixed height container for title */}
432
+ <div className="h-6 overflow-hidden flex items-center justify-center mb-1">
433
+ <motion.h3
434
+ key={isUnivAiPlusMode ? 'plus' : 'regular'}
435
+ initial={{ opacity: 0 }}
436
+ animate={{ opacity: 1 }}
437
+ transition={{ duration: 0.2 }}
438
+ className={`text-sm transition-all duration-300 whitespace-nowrap flex items-center ${
439
+ isUnivAiPlusMode
440
+ ? 'text-purple-900 dark:text-purple-100'
441
+ : 'text-red-900 dark:text-red-100'
442
+ }`}
443
+ >
444
+ {isUnivAiPlusMode ? 'UnivAi+++' : 'UnivAi'}
445
+ {isUnivAiPlusMode && <Crown className="ml-1" size={12} />}
446
+ </motion.h3>
447
+ </div>
448
+ {/* Fixed height container for description */}
449
+ <div className="h-5 overflow-hidden flex items-center justify-center mb-3">
450
+ <motion.p
451
+ key={isUnivAiPlusMode ? 'plus-desc' : 'regular-desc'}
452
+ initial={{ opacity: 0 }}
453
+ animate={{ opacity: 1 }}
454
+ transition={{ duration: 0.2 }}
455
+ className={`text-xs text-center transition-all duration-300 ${
456
+ isUnivAiPlusMode
457
+ ? 'text-purple-700/80 dark:text-purple-300/80'
458
+ : 'text-red-700/80 dark:text-red-300/80'
459
+ }`}
460
+ >
461
+ {isUnivAiPlusMode
462
+ ? 'Smarter with human-like responses'
463
+ : 'Any PUP-Related Queries?'
464
+ }
465
+ </motion.p>
466
+ </div>
467
+ {/* Fixed button */}
468
+ <div className="flex justify-center">
469
+ <Button
470
+ size="sm"
471
+ onClick={toggleUnivAiMode}
472
+ className={`text-white border-0 shadow-lg transition-all duration-300 text-xs px-3 py-1 w-40 h-7 ${
473
+ isUnivAiPlusMode
474
+ ? 'bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 shadow-purple-500/20'
475
+ : 'bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 shadow-red-500/20'
476
+ }`}
477
+ >
478
+ <span className="truncate">
479
+ {isUnivAiPlusMode ? 'Switch to UnivAi' : 'Try the New UnivAi+++'}
480
+ </span>
481
+ </Button>
482
+ </div>
483
+ </div>
484
+ </div>
485
+
486
+ {/* Chat Stats - Equalized for both modes */}
487
+ <div className={`rounded-xl p-3 border backdrop-blur-sm shadow-lg transition-all duration-300 ${
488
+ isUnivAiPlusMode
489
+ ? 'bg-gradient-to-br from-white/90 to-pink-50/90 border-pink-200/50 shadow-pink-100/30 dark:from-gray-800/90 dark:to-pink-900/90 dark:border-pink-600/50 dark:shadow-pink-900/30'
490
+ : 'bg-gradient-to-br from-white/90 to-orange-50/90 border-orange-200/50 shadow-orange-100/30 dark:from-gray-800/90 dark:to-gray-700/90 dark:border-gray-600/50 dark:shadow-gray-900/30'
491
+ }`}>
492
+ <h4 className={`mb-2 text-sm transition-all duration-300 ${
493
+ isUnivAiPlusMode
494
+ ? 'text-purple-900 dark:text-purple-100'
495
+ : 'text-red-900 dark:text-red-100'
496
+ }`}>Chat Statistics</h4>
497
+ <div className="space-y-2">
498
+ <div className="flex justify-between">
499
+ <span className={`text-xs transition-all duration-300 ${
500
+ isUnivAiPlusMode
501
+ ? 'text-purple-700/80 dark:text-purple-300/80'
502
+ : 'text-red-700/80 dark:text-red-300/80'
503
+ }`}>Response Time</span>
504
+ <span className={`text-xs transition-all duration-300 ${
505
+ isUnivAiPlusMode
506
+ ? 'text-purple-900 dark:text-purple-100'
507
+ : 'text-red-900 dark:text-red-100'
508
+ }`}>{isUnivAiPlusMode ? '2.5s' : '1.2s'}</span>
509
+ </div>
510
+ <div className="flex justify-between">
511
+ <span className={`text-xs transition-all duration-300 ${
512
+ isUnivAiPlusMode
513
+ ? 'text-purple-700/80 dark:text-purple-300/80'
514
+ : 'text-red-700/80 dark:text-red-300/80'
515
+ }`}>Status</span>
516
+ <span className="text-green-600 text-xs">{isUnivAiPlusMode ? 'Premium' : 'Online'}</span>
517
+ </div>
518
+ <div className="flex justify-between">
519
+ <span className={`text-xs transition-all duration-300 ${
520
+ isUnivAiPlusMode
521
+ ? 'text-purple-700/80 dark:text-purple-300/80'
522
+ : 'text-red-700/80 dark:text-red-300/80'
523
+ }}`}>{isUnivAiPlusMode ? 'Responses Left' : 'Queries Processed'}</span>
524
+ <span className={`text-xs transition-all duration-300 ${
525
+ isUnivAiPlusMode
526
+ ? 'text-purple-900 dark:text-purple-100'
527
+ : 'text-red-900 dark:text-red-100'
528
+ }`}>{isUnivAiPlusMode ? (hasUsedPlusResponse ? '0' : '1') : '∞'}</span>
529
+ </div>
530
+ </div>
531
+ </div>
532
+ </motion.div>
533
+ </div>
534
+ </div>
535
+ </div>
536
+ );
537
+ }
src/components/FeedbackPopup.tsx ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'motion/react';
3
+ import { ThumbsUp, ThumbsDown, X, MessageSquare } from 'lucide-react';
4
+ import { Button } from './ui/button';
5
+ import { Textarea } from './ui/textarea';
6
+
7
+ interface FeedbackPopupProps {
8
+ isOpen: boolean;
9
+ onClose: () => void;
10
+ messageId: string;
11
+ isUnivAiPlusMode?: boolean;
12
+ }
13
+
14
+ export function FeedbackPopup({ isOpen, onClose, messageId, isUnivAiPlusMode }: FeedbackPopupProps) {
15
+ const [feedbackType, setFeedbackType] = useState<'positive' | 'negative' | null>(null);
16
+ const [comment, setComment] = useState('');
17
+ const [isSubmitted, setIsSubmitted] = useState(false);
18
+
19
+ const handleSubmit = () => {
20
+ // Here you would typically send feedback to your backend
21
+ console.log('Feedback submitted:', { messageId, feedbackType, comment });
22
+ setIsSubmitted(true);
23
+ setTimeout(() => {
24
+ setIsSubmitted(false);
25
+ setFeedbackType(null);
26
+ setComment('');
27
+ onClose();
28
+ }, 1500);
29
+ };
30
+
31
+ const handleClose = () => {
32
+ setFeedbackType(null);
33
+ setComment('');
34
+ setIsSubmitted(false);
35
+ onClose();
36
+ };
37
+
38
+ return (
39
+ <AnimatePresence>
40
+ {isOpen && (
41
+ <>
42
+ {/* Backdrop */}
43
+ <motion.div
44
+ initial={{ opacity: 0 }}
45
+ animate={{ opacity: 1 }}
46
+ exit={{ opacity: 0 }}
47
+ className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
48
+ onClick={handleClose}
49
+ />
50
+
51
+ {/* Popup */}
52
+ <motion.div
53
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
54
+ animate={{ opacity: 1, scale: 1, y: 0 }}
55
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
56
+ className={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 w-96 rounded-2xl border backdrop-blur-sm shadow-2xl transition-all duration-300 ${
57
+ isUnivAiPlusMode
58
+ ? 'bg-gradient-to-br from-white/95 to-purple-50/95 border-purple-200/50 shadow-purple-500/20'
59
+ : 'bg-gradient-to-br from-white/95 to-red-50/95 border-red-200/50 shadow-red-500/20'
60
+ }`}
61
+ >
62
+ {/* Header */}
63
+ <div className="flex items-center justify-between p-6 border-b border-current/10">
64
+ <div className="flex items-center gap-3">
65
+ <div className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-300 ${
66
+ isUnivAiPlusMode
67
+ ? 'bg-gradient-to-r from-purple-500 to-pink-500 shadow-purple-500/30'
68
+ : 'bg-gradient-to-r from-red-500 to-orange-500 shadow-red-500/30'
69
+ }`}>
70
+ <MessageSquare className="text-white" size={20} />
71
+ </div>
72
+ <div>
73
+ <h3 className={`transition-all duration-300 ${
74
+ isUnivAiPlusMode
75
+ ? 'text-purple-900 dark:text-purple-100'
76
+ : 'text-red-900 dark:text-red-100'
77
+ }`}>
78
+ Share Feedback
79
+ </h3>
80
+ <p className={`text-sm transition-all duration-300 ${
81
+ isUnivAiPlusMode
82
+ ? 'text-purple-700/80 dark:text-purple-300/80'
83
+ : 'text-red-700/80 dark:text-red-300/80'
84
+ }`}>
85
+ Help us improve our responses
86
+ </p>
87
+ </div>
88
+ </div>
89
+ <Button
90
+ variant="ghost"
91
+ size="sm"
92
+ onClick={handleClose}
93
+ className={`transition-all duration-300 ${
94
+ isUnivAiPlusMode
95
+ ? 'hover:bg-purple-100 text-purple-600 dark:hover:bg-purple-900/30 dark:text-purple-400'
96
+ : 'hover:bg-red-100 text-red-600 dark:hover:bg-red-900/30 dark:text-red-400'
97
+ }`}
98
+ >
99
+ <X size={18} />
100
+ </Button>
101
+ </div>
102
+
103
+ {/* Content */}
104
+ <div className="p-6">
105
+ {!isSubmitted ? (
106
+ <>
107
+ {/* Rating */}
108
+ <div className="mb-4">
109
+ <p className={`text-sm mb-3 transition-all duration-300 ${
110
+ isUnivAiPlusMode
111
+ ? 'text-purple-700/80 dark:text-purple-300/80'
112
+ : 'text-red-700/80 dark:text-red-300/80'
113
+ }`}>
114
+ How was this response?
115
+ </p>
116
+ <div className="flex gap-3">
117
+ <Button
118
+ variant={feedbackType === 'positive' ? 'default' : 'outline'}
119
+ size="sm"
120
+ onClick={() => setFeedbackType('positive')}
121
+ className={`flex items-center gap-2 transition-all duration-300 ${
122
+ feedbackType === 'positive'
123
+ ? isUnivAiPlusMode
124
+ ? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-purple-500/30'
125
+ : 'bg-gradient-to-r from-red-500 to-orange-500 text-white shadow-red-500/30'
126
+ : isUnivAiPlusMode
127
+ ? 'border-purple-200 text-purple-700 hover:bg-purple-50 dark:border-purple-600 dark:text-purple-300 dark:hover:bg-purple-900/30'
128
+ : 'border-red-200 text-red-700 hover:bg-red-50 dark:border-red-600 dark:text-red-300 dark:hover:bg-red-900/30'
129
+ }`}
130
+ >
131
+ <ThumbsUp size={16} />
132
+ Helpful
133
+ </Button>
134
+ <Button
135
+ variant={feedbackType === 'negative' ? 'default' : 'outline'}
136
+ size="sm"
137
+ onClick={() => setFeedbackType('negative')}
138
+ className={`flex items-center gap-2 transition-all duration-300 ${
139
+ feedbackType === 'negative'
140
+ ? isUnivAiPlusMode
141
+ ? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-purple-500/30'
142
+ : 'bg-gradient-to-r from-red-500 to-orange-500 text-white shadow-red-500/30'
143
+ : isUnivAiPlusMode
144
+ ? 'border-purple-200 text-purple-700 hover:bg-purple-50 dark:border-purple-600 dark:text-purple-300 dark:hover:bg-purple-900/30'
145
+ : 'border-red-200 text-red-700 hover:bg-red-50 dark:border-red-600 dark:text-red-300 dark:hover:bg-red-900/30'
146
+ }`}
147
+ >
148
+ <ThumbsDown size={16} />
149
+ Not helpful
150
+ </Button>
151
+ </div>
152
+ </div>
153
+
154
+ {/* Comment */}
155
+ <div className="mb-4">
156
+ <p className={`text-sm mb-2 transition-all duration-300 ${
157
+ isUnivAiPlusMode
158
+ ? 'text-purple-700/80 dark:text-purple-300/80'
159
+ : 'text-red-700/80 dark:text-red-300/80'
160
+ }`}>
161
+ Additional comments (optional)
162
+ </p>
163
+ <Textarea
164
+ value={comment}
165
+ onChange={(e) => setComment(e.target.value)}
166
+ placeholder="Tell us more about your experience..."
167
+ className={`transition-all duration-300 ${
168
+ isUnivAiPlusMode
169
+ ? 'border-purple-200 focus:border-purple-400 dark:border-purple-600'
170
+ : 'border-red-200 focus:border-red-400 dark:border-red-600'
171
+ }`}
172
+ rows={3}
173
+ />
174
+ </div>
175
+
176
+ {/* Actions */}
177
+ <div className="flex gap-3 justify-end">
178
+ <Button
179
+ variant="outline"
180
+ size="sm"
181
+ onClick={handleClose}
182
+ className={`transition-all duration-300 ${
183
+ isUnivAiPlusMode
184
+ ? 'border-purple-200 text-purple-700 hover:bg-purple-50 dark:border-purple-600 dark:text-purple-300 dark:hover:bg-purple-900/30'
185
+ : 'border-red-200 text-red-700 hover:bg-red-50 dark:border-red-600 dark:text-red-300 dark:hover:bg-red-900/30'
186
+ }`}
187
+ >
188
+ Cancel
189
+ </Button>
190
+ <Button
191
+ size="sm"
192
+ onClick={handleSubmit}
193
+ disabled={!feedbackType}
194
+ className={`transition-all duration-300 ${
195
+ isUnivAiPlusMode
196
+ ? 'bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white shadow-purple-500/30'
197
+ : 'bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white shadow-red-500/30'
198
+ }`}
199
+ >
200
+ Submit Feedback
201
+ </Button>
202
+ </div>
203
+ </>
204
+ ) : (
205
+ /* Success state */
206
+ <motion.div
207
+ initial={{ opacity: 0, y: 10 }}
208
+ animate={{ opacity: 1, y: 0 }}
209
+ className="text-center py-4"
210
+ >
211
+ <div className={`w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 transition-all duration-300 ${
212
+ isUnivAiPlusMode
213
+ ? 'bg-gradient-to-r from-purple-400 to-pink-500 shadow-purple-500/30'
214
+ : 'bg-gradient-to-r from-green-400 to-green-500 shadow-green-500/30'
215
+ }`}>
216
+ <ThumbsUp className="text-white" size={24} />
217
+ </div>
218
+ <h4 className={`mb-2 transition-all duration-300 ${
219
+ isUnivAiPlusMode
220
+ ? 'text-purple-900 dark:text-purple-100'
221
+ : 'text-red-900 dark:text-red-100'
222
+ }`}>
223
+ Thank you!
224
+ </h4>
225
+ <p className={`text-sm transition-all duration-300 ${
226
+ isUnivAiPlusMode
227
+ ? 'text-purple-700/80 dark:text-purple-300/80'
228
+ : 'text-red-700/80 dark:text-red-300/80'
229
+ }`}>
230
+ Your feedback helps us improve
231
+ </p>
232
+ </motion.div>
233
+ )}
234
+ </div>
235
+ </motion.div>
236
+ </>
237
+ )}
238
+ </AnimatePresence>
239
+ );
240
+ }
src/components/Message.tsx ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { motion } from 'motion/react';
3
+ import { Bot, User, Crown, Sparkles, MessageSquare } from 'lucide-react';
4
+ import { Button } from './ui/button';
5
+ import { FeedbackPopup } from './FeedbackPopup';
6
+
7
+ export interface MessageProps {
8
+ content: string;
9
+ isUser: boolean;
10
+ timestamp: Date;
11
+ isPlusResponse?: boolean;
12
+ isUnivAiPlusMode?: boolean;
13
+ }
14
+
15
+ export function Message({ content, isUser, timestamp, isPlusResponse, isUnivAiPlusMode }: MessageProps) {
16
+ const [showFeedback, setShowFeedback] = useState(false);
17
+
18
+ return (
19
+ <>
20
+ <motion.div
21
+ initial={{ opacity: 0, y: 20 }}
22
+ animate={{ opacity: 1, y: 0 }}
23
+ transition={{ duration: 0.3 }}
24
+ className={`flex items-start gap-3 mb-4 ${isUser ? 'flex-row-reverse' : 'flex-row'}`}
25
+ >
26
+ <div className={`
27
+ flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center relative transition-all duration-300
28
+ ${isUser
29
+ ? isUnivAiPlusMode
30
+ ? 'bg-gradient-to-r from-purple-500 to-purple-600 text-white shadow-lg shadow-purple-500/30'
31
+ : 'bg-gradient-to-r from-red-500 to-red-600 text-white shadow-lg shadow-red-500/30'
32
+ : isPlusResponse
33
+ ? 'bg-gradient-to-r from-purple-400 to-pink-500 text-white shadow-lg shadow-purple-500/30'
34
+ : isUnivAiPlusMode
35
+ ? 'bg-gradient-to-r from-purple-400 to-pink-500 text-white shadow-lg shadow-purple-500/30'
36
+ : 'bg-gradient-to-r from-yellow-400 to-orange-500 text-white shadow-lg shadow-yellow-500/30'
37
+ }
38
+ `}>
39
+ {isUser ? (
40
+ <User size={20} />
41
+ ) : isPlusResponse ? (
42
+ <Crown size={20} />
43
+ ) : isUnivAiPlusMode ? (
44
+ <Crown size={20} />
45
+ ) : (
46
+ <Bot size={20} />
47
+ )}
48
+
49
+ {/* Premium indicator sparkle */}
50
+ {isPlusResponse && (
51
+ <motion.div
52
+ initial={{ scale: 0, opacity: 0 }}
53
+ animate={{ scale: 1, opacity: 1 }}
54
+ className="absolute -top-1 -right-1 w-4 h-4 bg-gradient-to-r from-amber-400 to-yellow-500 rounded-full flex items-center justify-center"
55
+ >
56
+ <Sparkles size={10} className="text-white" />
57
+ </motion.div>
58
+ )}
59
+ </div>
60
+
61
+ <div className={`flex flex-col max-w-[70%] ${isUser ? 'items-end' : 'items-start'}`}>
62
+ {/* Premium response indicator - moved outside and above the message bubble */}
63
+ {isPlusResponse && (
64
+ <motion.div
65
+ initial={{ opacity: 0, scale: 0.8 }}
66
+ animate={{ opacity: 1, scale: 1 }}
67
+ className="flex items-center gap-1 px-3 py-1 mb-2 bg-gradient-to-r from-purple-500 to-pink-500 text-white text-xs rounded-full shadow-lg mr-4"
68
+ >
69
+ <Crown size={12} />
70
+ <span>Premium Response</span>
71
+ </motion.div>
72
+ )}
73
+
74
+ <div className={`
75
+ px-4 py-3 rounded-2xl backdrop-blur-sm border shadow-sm transition-all duration-300 relative
76
+ ${isUser
77
+ ? isUnivAiPlusMode
78
+ ? 'bg-gradient-to-r from-purple-50 to-purple-100 border-purple-200 text-purple-900 ml-4'
79
+ : 'bg-gradient-to-r from-red-50 to-red-100 border-red-200 text-red-900 ml-4'
80
+ : isPlusResponse
81
+ ? 'bg-gradient-to-r from-purple-50 via-pink-50 to-amber-50 border-purple-300 text-purple-900 mr-4 shadow-lg shadow-purple-200/50'
82
+ : isUnivAiPlusMode
83
+ ? 'bg-gradient-to-r from-white to-purple-50 border-purple-200 text-purple-900 mr-4'
84
+ : 'bg-gradient-to-r from-white to-yellow-50 border-yellow-200 text-red-900 mr-4'
85
+ }
86
+ `}>
87
+ <p className="whitespace-pre-wrap">
88
+ {content}
89
+ </p>
90
+ </div>
91
+
92
+ <div className={`flex items-center gap-2 mt-1 px-2 ${isUser ? 'flex-row-reverse' : 'flex-row'}`}>
93
+ <span className={`text-xs transition-all duration-300 ${
94
+ isUnivAiPlusMode
95
+ ? 'text-purple-600/60'
96
+ : 'text-red-600/60'
97
+ }`}>
98
+ {timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
99
+ {isPlusResponse && <span className="ml-2 text-purple-600/80">• Premium</span>}
100
+ </span>
101
+
102
+ {/* Feedback button for bot responses */}
103
+ {!isUser && (
104
+ <Button
105
+ variant="ghost"
106
+ size="sm"
107
+ onClick={() => setShowFeedback(true)}
108
+ className={`text-xs p-1 h-6 transition-all duration-300 opacity-60 hover:opacity-100 ${
109
+ isUnivAiPlusMode
110
+ ? 'hover:bg-purple-100 text-purple-600 dark:hover:bg-purple-900/30 dark:text-purple-400'
111
+ : 'hover:bg-red-100 text-red-600 dark:hover:bg-red-900/30 dark:text-red-400'
112
+ }`}
113
+ >
114
+ <MessageSquare size={12} />
115
+ </Button>
116
+ )}
117
+ </div>
118
+ </div>
119
+ </motion.div>
120
+
121
+ {/* Feedback Popup */}
122
+ <FeedbackPopup
123
+ isOpen={showFeedback}
124
+ onClose={() => setShowFeedback(false)}
125
+ messageId={`msg-${timestamp.getTime()}`}
126
+ isUnivAiPlusMode={isUnivAiPlusMode}
127
+ />
128
+ </>
129
+ );
130
+ }
src/components/TypingIndicator.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from 'motion/react';
2
+ import { Bot, Crown } from 'lucide-react';
3
+
4
+ interface TypingIndicatorProps {
5
+ isUnivAiPlusMode?: boolean;
6
+ }
7
+
8
+ export function TypingIndicator({ isUnivAiPlusMode }: TypingIndicatorProps) {
9
+ return (
10
+ <motion.div
11
+ initial={{ opacity: 0, y: 20 }}
12
+ animate={{ opacity: 1, y: 0 }}
13
+ transition={{ duration: 0.3 }}
14
+ className="flex items-start gap-3 mb-4"
15
+ >
16
+ <div className={`
17
+ flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center transition-all duration-300
18
+ ${isUnivAiPlusMode
19
+ ? 'bg-gradient-to-r from-purple-400 to-pink-500 text-white shadow-lg shadow-purple-500/30'
20
+ : 'bg-gradient-to-r from-yellow-400 to-orange-500 text-white shadow-lg shadow-yellow-500/30'
21
+ }
22
+ `}>
23
+ {isUnivAiPlusMode ? <Crown size={20} /> : <Bot size={20} />}
24
+ </div>
25
+
26
+ <div className={`
27
+ px-4 py-3 rounded-2xl backdrop-blur-sm border shadow-sm mr-4 transition-all duration-300
28
+ ${isUnivAiPlusMode
29
+ ? 'border-purple-200 bg-gradient-to-r from-white to-purple-50 text-purple-900'
30
+ : 'border-yellow-200 bg-gradient-to-r from-white to-yellow-50 text-red-900'
31
+ }
32
+ `}>
33
+ <div className="flex space-x-1">
34
+ <motion.div
35
+ className={`w-2 h-2 rounded-full ${
36
+ isUnivAiPlusMode ? 'bg-purple-500/80' : 'bg-red-500/80'
37
+ }`}
38
+ animate={{ y: [0, -5, 0] }}
39
+ transition={{ duration: 0.6, repeat: Infinity, delay: 0 }}
40
+ />
41
+ <motion.div
42
+ className={`w-2 h-2 rounded-full ${
43
+ isUnivAiPlusMode ? 'bg-pink-500/80' : 'bg-orange-500/80'
44
+ }`}
45
+ animate={{ y: [0, -5, 0] }}
46
+ transition={{ duration: 0.6, repeat: Infinity, delay: 0.1 }}
47
+ />
48
+ <motion.div
49
+ className={`w-2 h-2 rounded-full ${
50
+ isUnivAiPlusMode ? 'bg-amber-500/80' : 'bg-yellow-500/80'
51
+ }`}
52
+ animate={{ y: [0, -5, 0] }}
53
+ transition={{ duration: 0.6, repeat: Infinity, delay: 0.2 }}
54
+ />
55
+ </div>
56
+ </div>
57
+ </motion.div>
58
+ );
59
+ }
src/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/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@1.2.3";
5
+ import { ChevronDownIcon } from "lucide-react@0.487.0";
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/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@1.1.6";
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/components/ui/alert.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority@0.7.1";
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/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio@1.1.2";
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/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@1.1.3";
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/components/ui/badge.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot@1.1.2";
3
+ import { cva, type VariantProps } from "class-variance-authority@0.7.1";
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/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot@1.1.2";
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react@0.487.0";
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/components/ui/button.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot@1.1.2";
3
+ import { cva, type VariantProps } from "class-variance-authority@0.7.1";
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/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@0.487.0";
5
+ import { DayPicker } from "react-day-picker@8.10.1";
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/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/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@8.6.0";
7
+ import { ArrowLeft, ArrowRight } from "lucide-react@0.487.0";
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/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@2.15.2";
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/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@1.1.4";
5
+ import { CheckIcon } from "lucide-react@0.487.0";
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/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible@1.1.3";
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/components/ui/command.tsx ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // If you have fetch calls, update them to /api/chat and /api/feedback
168
+
169
+ export {
170
+ Command,
171
+ CommandDialog,
172
+ CommandInput,
173
+ CommandList,
174
+ CommandEmpty,
175
+ CommandGroup,
176
+ CommandItem,
177
+ CommandShortcut,
178
+ CommandSeparator,
179
+ };
src/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@2.2.6";
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
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/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@1.1.6";
5
+ import { XIcon } from "lucide-react@0.487.0";
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/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@1.1.2";
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/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@2.1.6";
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
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/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/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@1.1.6";
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/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@1.4.2";
5
+ import { MinusIcon } from "lucide-react@0.487.0";
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/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/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@2.1.2";
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/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@1.1.6";
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
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/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@1.2.5";
3
+ import { cva } from "class-variance-authority@0.7.1";
4
+ import { ChevronDownIcon } from "lucide-react@0.487.0";
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/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@0.487.0";
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/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@1.1.6";
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/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@1.1.2";
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 };
src/components/ui/radio-group.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group@1.2.3";
5
+ import { CircleIcon } from "lucide-react@0.487.0";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function RadioGroup({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
13
+ return (
14
+ <RadioGroupPrimitive.Root
15
+ data-slot="radio-group"
16
+ className={cn("grid gap-3", className)}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function RadioGroupItem({
23
+ className,
24
+ ...props
25
+ }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
26
+ return (
27
+ <RadioGroupPrimitive.Item
28
+ data-slot="radio-group-item"
29
+ className={cn(
30
+ "border-input text-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 dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
31
+ className,
32
+ )}
33
+ {...props}
34
+ >
35
+ <RadioGroupPrimitive.Indicator
36
+ data-slot="radio-group-indicator"
37
+ className="relative flex items-center justify-center"
38
+ >
39
+ <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
40
+ </RadioGroupPrimitive.Indicator>
41
+ </RadioGroupPrimitive.Item>
42
+ );
43
+ }
44
+
45
+ export { RadioGroup, RadioGroupItem };
src/components/ui/resizable.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { GripVerticalIcon } from "lucide-react@0.487.0";
5
+ import * as ResizablePrimitive from "react-resizable-panels@2.1.7";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function ResizablePanelGroup({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
13
+ return (
14
+ <ResizablePrimitive.PanelGroup
15
+ data-slot="resizable-panel-group"
16
+ className={cn(
17
+ "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ function ResizablePanel({
26
+ ...props
27
+ }: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
28
+ return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
29
+ }
30
+
31
+ function ResizableHandle({
32
+ withHandle,
33
+ className,
34
+ ...props
35
+ }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
36
+ withHandle?: boolean;
37
+ }) {
38
+ return (
39
+ <ResizablePrimitive.PanelResizeHandle
40
+ data-slot="resizable-handle"
41
+ className={cn(
42
+ "bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
43
+ className,
44
+ )}
45
+ {...props}
46
+ >
47
+ {withHandle && (
48
+ <div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
49
+ <GripVerticalIcon className="size-2.5" />
50
+ </div>
51
+ )}
52
+ </ResizablePrimitive.PanelResizeHandle>
53
+ );
54
+ }
55
+
56
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
src/components/ui/scroll-area.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area@1.2.3";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function ScrollArea({
9
+ className,
10
+ children,
11
+ ...props
12
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
13
+ return (
14
+ <ScrollAreaPrimitive.Root
15
+ data-slot="scroll-area"
16
+ className={cn("relative", className)}
17
+ {...props}
18
+ >
19
+ <ScrollAreaPrimitive.Viewport
20
+ data-slot="scroll-area-viewport"
21
+ className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
22
+ >
23
+ {children}
24
+ </ScrollAreaPrimitive.Viewport>
25
+ <ScrollBar />
26
+ <ScrollAreaPrimitive.Corner />
27
+ </ScrollAreaPrimitive.Root>
28
+ );
29
+ }
30
+
31
+ function ScrollBar({
32
+ className,
33
+ orientation = "vertical",
34
+ ...props
35
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
36
+ return (
37
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
38
+ data-slot="scroll-area-scrollbar"
39
+ orientation={orientation}
40
+ className={cn(
41
+ "flex touch-none p-px transition-colors select-none",
42
+ orientation === "vertical" &&
43
+ "h-full w-2.5 border-l border-l-transparent",
44
+ orientation === "horizontal" &&
45
+ "h-2.5 flex-col border-t border-t-transparent",
46
+ className,
47
+ )}
48
+ {...props}
49
+ >
50
+ <ScrollAreaPrimitive.ScrollAreaThumb
51
+ data-slot="scroll-area-thumb"
52
+ className="bg-border relative flex-1 rounded-full"
53
+ />
54
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
55
+ );
56
+ }
57
+
58
+ export { ScrollArea, ScrollBar };
src/components/ui/select.tsx ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SelectPrimitive from "@radix-ui/react-select@2.1.6";
5
+ import {
6
+ CheckIcon,
7
+ ChevronDownIcon,
8
+ ChevronUpIcon,
9
+ } from "lucide-react@0.487.0";
10
+
11
+ import { cn } from "./utils";
12
+
13
+ function Select({
14
+ ...props
15
+ }: React.ComponentProps<typeof SelectPrimitive.Root>) {
16
+ return <SelectPrimitive.Root data-slot="select" {...props} />;
17
+ }
18
+
19
+ function SelectGroup({
20
+ ...props
21
+ }: React.ComponentProps<typeof SelectPrimitive.Group>) {
22
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />;
23
+ }
24
+
25
+ function SelectValue({
26
+ ...props
27
+ }: React.ComponentProps<typeof SelectPrimitive.Value>) {
28
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />;
29
+ }
30
+
31
+ function SelectTrigger({
32
+ className,
33
+ size = "default",
34
+ children,
35
+ ...props
36
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
37
+ size?: "sm" | "default";
38
+ }) {
39
+ return (
40
+ <SelectPrimitive.Trigger
41
+ data-slot="select-trigger"
42
+ data-size={size}
43
+ className={cn(
44
+ "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-input-background px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
45
+ className,
46
+ )}
47
+ {...props}
48
+ >
49
+ {children}
50
+ <SelectPrimitive.Icon asChild>
51
+ <ChevronDownIcon className="size-4 opacity-50" />
52
+ </SelectPrimitive.Icon>
53
+ </SelectPrimitive.Trigger>
54
+ );
55
+ }
56
+
57
+ function SelectContent({
58
+ className,
59
+ children,
60
+ position = "popper",
61
+ ...props
62
+ }: React.ComponentProps<typeof SelectPrimitive.Content>) {
63
+ return (
64
+ <SelectPrimitive.Portal>
65
+ <SelectPrimitive.Content
66
+ data-slot="select-content"
67
+ className={cn(
68
+ "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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
69
+ position === "popper" &&
70
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
71
+ className,
72
+ )}
73
+ position={position}
74
+ {...props}
75
+ >
76
+ <SelectScrollUpButton />
77
+ <SelectPrimitive.Viewport
78
+ className={cn(
79
+ "p-1",
80
+ position === "popper" &&
81
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
82
+ )}
83
+ >
84
+ {children}
85
+ </SelectPrimitive.Viewport>
86
+ <SelectScrollDownButton />
87
+ </SelectPrimitive.Content>
88
+ </SelectPrimitive.Portal>
89
+ );
90
+ }
91
+
92
+ function SelectLabel({
93
+ className,
94
+ ...props
95
+ }: React.ComponentProps<typeof SelectPrimitive.Label>) {
96
+ return (
97
+ <SelectPrimitive.Label
98
+ data-slot="select-label"
99
+ className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
100
+ {...props}
101
+ />
102
+ );
103
+ }
104
+
105
+ function SelectItem({
106
+ className,
107
+ children,
108
+ ...props
109
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
110
+ return (
111
+ <SelectPrimitive.Item
112
+ data-slot="select-item"
113
+ className={cn(
114
+ "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 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 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
115
+ className,
116
+ )}
117
+ {...props}
118
+ >
119
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
120
+ <SelectPrimitive.ItemIndicator>
121
+ <CheckIcon className="size-4" />
122
+ </SelectPrimitive.ItemIndicator>
123
+ </span>
124
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
125
+ </SelectPrimitive.Item>
126
+ );
127
+ }
128
+
129
+ function SelectSeparator({
130
+ className,
131
+ ...props
132
+ }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
133
+ return (
134
+ <SelectPrimitive.Separator
135
+ data-slot="select-separator"
136
+ className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
137
+ {...props}
138
+ />
139
+ );
140
+ }
141
+
142
+ function SelectScrollUpButton({
143
+ className,
144
+ ...props
145
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
146
+ return (
147
+ <SelectPrimitive.ScrollUpButton
148
+ data-slot="select-scroll-up-button"
149
+ className={cn(
150
+ "flex cursor-default items-center justify-center py-1",
151
+ className,
152
+ )}
153
+ {...props}
154
+ >
155
+ <ChevronUpIcon className="size-4" />
156
+ </SelectPrimitive.ScrollUpButton>
157
+ );
158
+ }
159
+
160
+ function SelectScrollDownButton({
161
+ className,
162
+ ...props
163
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
164
+ return (
165
+ <SelectPrimitive.ScrollDownButton
166
+ data-slot="select-scroll-down-button"
167
+ className={cn(
168
+ "flex cursor-default items-center justify-center py-1",
169
+ className,
170
+ )}
171
+ {...props}
172
+ >
173
+ <ChevronDownIcon className="size-4" />
174
+ </SelectPrimitive.ScrollDownButton>
175
+ );
176
+ }
177
+
178
+ export {
179
+ Select,
180
+ SelectContent,
181
+ SelectGroup,
182
+ SelectItem,
183
+ SelectLabel,
184
+ SelectScrollDownButton,
185
+ SelectScrollUpButton,
186
+ SelectSeparator,
187
+ SelectTrigger,
188
+ SelectValue,
189
+ };
src/components/ui/separator.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SeparatorPrimitive from "@radix-ui/react-separator@1.1.2";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function Separator({
9
+ className,
10
+ orientation = "horizontal",
11
+ decorative = true,
12
+ ...props
13
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
14
+ return (
15
+ <SeparatorPrimitive.Root
16
+ data-slot="separator-root"
17
+ decorative={decorative}
18
+ orientation={orientation}
19
+ className={cn(
20
+ "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ export { Separator };