ChenyuRabbitLove commited on
Commit
5081fcb
·
1 Parent(s): e0d6b69

feat: optimize for-students and for-teachers page

Browse files
Files changed (47) hide show
  1. .eslintrc.json +1 -1
  2. .gitignore +2 -1
  3. .prettierignore +3 -0
  4. .prettierrc +1 -0
  5. app/(audiences)/components/chat-bot-card.tsx +58 -0
  6. components/custom/ChatBotGridSkeleton.tsx → app/(audiences)/components/chat-bot-grid-skeleton.tsx +2 -2
  7. app/(audiences)/components/chat-bot-grid.tsx +156 -0
  8. components/custom/SearchBar.tsx → app/(audiences)/components/search-bar.tsx +11 -6
  9. app/(audiences)/components/subject-faceted-filter.tsx +145 -0
  10. app/(audiences)/components/subject-filter-view-options.tsx +59 -0
  11. app/(audiences)/components/subject-filter.tsx +59 -0
  12. app/(audiences)/for-students/data/chatbots.ts +142 -0
  13. app/(audiences)/for-students/data/data.tsx +49 -0
  14. app/{for-students → (audiences)/for-students}/page.tsx +38 -12
  15. app/(audiences)/for-teachers/data/chatbots.ts +66 -0
  16. app/(audiences)/for-teachers/page.tsx +62 -0
  17. app/components/chatbot/chat-bot-card.tsx +2 -0
  18. app/components/chatbot/chat-bot-grid.tsx +152 -0
  19. components/custom/LandingPageChatBot.tsx → app/components/landing-page-chat-bot.tsx +83 -61
  20. app/components/theme-provider.tsx +38 -0
  21. app/components/top-nav.tsx +127 -0
  22. app/globals.css +32 -32
  23. app/layout.tsx +12 -9
  24. app/page.tsx +174 -42
  25. app/types/chatbot.ts +10 -0
  26. components.json +1 -1
  27. components/custom/ChatBotCard.tsx +0 -43
  28. components/custom/ChatBotGrid.tsx +0 -60
  29. components/custom/SubjectFilter.tsx +0 -19
  30. components/custom/theme-provider.tsx +0 -22
  31. components/custom/top-nav.tsx +0 -96
  32. components/ui/badge.tsx +36 -0
  33. components/ui/button.tsx +14 -14
  34. components/ui/card.tsx +23 -16
  35. components/ui/command.tsx +153 -0
  36. components/ui/dialog.tsx +122 -0
  37. components/ui/dropdown-menu.tsx +41 -41
  38. components/ui/input.tsx +22 -0
  39. components/ui/popover.tsx +33 -0
  40. components/ui/separator.tsx +31 -0
  41. lib/utils.ts +3 -3
  42. next.config.ts +5 -5
  43. package.json +7 -0
  44. pnpm-lock.yaml +0 -0
  45. public/chatbots/code-tutor.webp +0 -0
  46. public/chatbots/whimsical.svg +1 -0
  47. tailwind.config.ts +71 -71
.eslintrc.json CHANGED
@@ -1,3 +1,3 @@
1
  {
2
- "extends": ["next/core-web-vitals", "next/typescript"]
3
  }
 
1
  {
2
+ "extends": ["next/core-web-vitals", "next/typescript", "prettier"]
3
  }
.gitignore CHANGED
@@ -50,4 +50,5 @@ venv/
50
  node_modules
51
 
52
  # local files
53
- /local_files
 
 
50
  node_modules
51
 
52
  # local files
53
+ /local_files
54
+ /prompts
.prettierignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Ignore artifacts:
2
+ build
3
+ coverage
.prettierrc ADDED
@@ -0,0 +1 @@
 
 
1
+ {}
app/(audiences)/components/chat-bot-card.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, CardContent } from "@/components/ui/card";
2
+ import Image from "next/image";
3
+ import { MessageSquareShare } from "lucide-react";
4
+ import { ChatBot } from "@/app/(audiences)/for-students/data/chatbots";
5
+
6
+ interface ChatBotCardProps {
7
+ chatbot: ChatBot;
8
+ }
9
+
10
+ export default function ChatBotCard({ chatbot }: ChatBotCardProps) {
11
+ const baseIconName = chatbot.icon.split(".")[0];
12
+
13
+ return (
14
+ <Card className="group relative overflow-hidden bg-background-primary/50 shadow-sm transition-all hover:shadow-xl hover:-translate-y-1 backdrop-blur-sm border-2 border-border/40 hover:border-primary/20 duration-300 aspect-square">
15
+ <CardContent className="p-8 h-full flex flex-col">
16
+ {/* Avatar with WebP and PNG fallback */}
17
+ <div className="flex-shrink-0 mb-6">
18
+ <picture>
19
+ <source
20
+ srcSet={`/chatbots/${baseIconName}.webp`}
21
+ type="image/webp"
22
+ />
23
+ <Image
24
+ src={`/chatbots/${baseIconName}.png`}
25
+ alt={chatbot.title}
26
+ width={64}
27
+ height={64}
28
+ className="relative rounded-lg transform group-hover:scale-105 transition duration-300"
29
+ />
30
+ </picture>
31
+ </div>
32
+
33
+ {/* Content */}
34
+ <div className="flex-1 flex flex-col">
35
+ <h3 className="text-xl font-bold text-text-primary truncate bg-gradient-to-r from-[#FF6B6B] via-[#4ECDC4] to-[#45B7D1] bg-clip-text text-transparent group-hover:tracking-wide transition-all duration-300 mb-3">
36
+ {chatbot.title}
37
+ </h3>
38
+
39
+ <p className="text-base text-text-secondary line-clamp-3 mb-6 flex-1 leading-relaxed">
40
+ {chatbot.description}
41
+ </p>
42
+
43
+ <button
44
+ className="flex-shrink-0 inline-flex items-center justify-center rounded-full bg-primary w-12 h-12 text-white transition-all duration-300 hover:scale-110 active:scale-95 hover:bg-primary/90 ml-auto"
45
+ aria-label="開始對話"
46
+ >
47
+ <MessageSquareShare className="w-6 h-6 group-hover:animate-pulse" />
48
+ </button>
49
+ </div>
50
+ </CardContent>
51
+
52
+ {/* Decorative corner accent */}
53
+ <div className="absolute top-0 right-0 w-20 h-20 overflow-hidden">
54
+ <div className="absolute top-0 right-0 w-16 h-16 bg-gradient-to-bl from-primary/10 via-primary/5 to-transparent transform rotate-45 translate-x-10 -translate-y-10 group-hover:translate-x-8 group-hover:-translate-y-8 transition-transform duration-300" />
55
+ </div>
56
+ </Card>
57
+ );
58
+ }
components/custom/ChatBotGridSkeleton.tsx → app/(audiences)/components/chat-bot-grid-skeleton.tsx RENAMED
@@ -21,5 +21,5 @@ export default function ChatBotGridSkeleton() {
21
  </div>
22
  ))}
23
  </div>
24
- )
25
- }
 
21
  </div>
22
  ))}
23
  </div>
24
+ );
25
+ }
app/(audiences)/components/chat-bot-grid.tsx ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import ChatBotCard from "./chat-bot-card";
5
+ import { ChatBot } from "@/app/(audiences)/for-students/data/chatbots";
6
+
7
+ interface SectionProps {
8
+ title: string;
9
+ subtitle: string;
10
+ bots: ChatBot[];
11
+ columns?: number;
12
+ }
13
+
14
+ function Section({ title, subtitle, bots, columns = 4 }: SectionProps) {
15
+ if (bots.length === 0) return null;
16
+
17
+ return (
18
+ <div className="mb-16">
19
+ <div className="mb-8">
20
+ <h2 className="text-2xl font-bold text-text-primary mb-2">{title}</h2>
21
+ <p className="text-text-secondary">{subtitle}</p>
22
+ </div>
23
+ <div className={`grid gap-4 sm:grid-cols-2 lg:grid-cols-${columns}`}>
24
+ {bots.map((chatbot) => (
25
+ <ChatBotCard key={chatbot.id} chatbot={chatbot} />
26
+ ))}
27
+ </div>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ interface ChatBotGridProps {
33
+ selectedCategories: string[];
34
+ selectedSubjects: string[];
35
+ searchQuery: string;
36
+ getPopularBots: () => ChatBot[];
37
+ getTrendingBots: () => ChatBot[];
38
+ getAllBots: () => ChatBot[];
39
+ }
40
+
41
+ export default function ChatBotGrid({
42
+ selectedCategories,
43
+ selectedSubjects,
44
+ searchQuery,
45
+ getPopularBots,
46
+ getTrendingBots,
47
+ getAllBots,
48
+ }: ChatBotGridProps) {
49
+ // Initialize state with the passed-in functions
50
+ const [filteredPopularBots, setFilteredPopularBots] = useState<ChatBot[]>(
51
+ () => getPopularBots(),
52
+ );
53
+ const [filteredTrendingBots, setFilteredTrendingBots] = useState<ChatBot[]>(
54
+ () => getTrendingBots(),
55
+ );
56
+ const [filteredAllBots, setFilteredAllBots] = useState<ChatBot[]>(() =>
57
+ getAllBots(),
58
+ );
59
+
60
+ useEffect(() => {
61
+ const filterBots = (bots: ChatBot[]) => {
62
+ let filtered = bots;
63
+
64
+ if (searchQuery) {
65
+ const query = searchQuery.toLowerCase();
66
+ filtered = filtered.filter(
67
+ (bot) =>
68
+ bot.title.toLowerCase().includes(query) ||
69
+ bot.description.toLowerCase().includes(query) ||
70
+ bot.subject.toLowerCase().includes(query) ||
71
+ bot.category.toLowerCase().includes(query),
72
+ );
73
+ }
74
+
75
+ if (selectedCategories.length > 0) {
76
+ filtered = filtered.filter(
77
+ (bot) => bot.category && selectedCategories.includes(bot.category),
78
+ );
79
+ }
80
+
81
+ if (selectedSubjects.length > 0) {
82
+ filtered = filtered.filter((bot) =>
83
+ selectedSubjects.includes(bot.subject),
84
+ );
85
+ }
86
+
87
+ return filtered;
88
+ };
89
+
90
+ setFilteredPopularBots(filterBots(getPopularBots()));
91
+ setFilteredTrendingBots(filterBots(getTrendingBots()));
92
+ setFilteredAllBots(filterBots(getAllBots()));
93
+ }, [
94
+ selectedCategories,
95
+ selectedSubjects,
96
+ searchQuery,
97
+ getPopularBots,
98
+ getTrendingBots,
99
+ getAllBots,
100
+ ]);
101
+
102
+ const hasFiltersOrSearch =
103
+ selectedCategories.length > 0 || selectedSubjects.length > 0 || searchQuery;
104
+
105
+ if (hasFiltersOrSearch) {
106
+ return (
107
+ <Section
108
+ title="搜尋結果"
109
+ subtitle={`符合 ${searchQuery ? '"' + searchQuery + '" ' : ""}的工具`}
110
+ bots={filteredAllBots}
111
+ columns={4}
112
+ />
113
+ );
114
+ }
115
+
116
+ const tutorBots = filteredAllBots.filter((bot) => bot.category === "教育");
117
+ const teachingBots = filteredAllBots.filter((bot) => bot.subject === "教學");
118
+ const assessmentBots = filteredAllBots.filter(
119
+ (bot) => bot.subject === "評量",
120
+ );
121
+
122
+ return (
123
+ <div className="space-y-8">
124
+ <Section
125
+ title="精選"
126
+ subtitle="歷來最多人使用的工具"
127
+ bots={filteredPopularBots}
128
+ columns={4}
129
+ />
130
+ <Section
131
+ title="熱門"
132
+ subtitle="近期最多人使用的工具"
133
+ bots={filteredTrendingBots}
134
+ columns={4}
135
+ />
136
+ <Section
137
+ title="教學輔助"
138
+ subtitle="教學規劃與課程設計工具"
139
+ bots={teachingBots}
140
+ columns={4}
141
+ />
142
+ <Section
143
+ title="評量工具"
144
+ subtitle="作業與評量相關工具"
145
+ bots={assessmentBots}
146
+ columns={4}
147
+ />
148
+ <Section
149
+ title="教育資源"
150
+ subtitle="其他教育相關工具"
151
+ bots={tutorBots}
152
+ columns={4}
153
+ />
154
+ </div>
155
+ );
156
+ }
components/custom/SearchBar.tsx → app/(audiences)/components/search-bar.tsx RENAMED
@@ -1,13 +1,18 @@
1
- 'use client'
2
 
3
- export default function SearchBar() {
 
 
 
 
4
  return (
5
  <div className="relative max-w-full mx-auto">
6
  <input
7
  type="text"
8
- placeholder="搜尋 GPT..."
 
9
  className="w-full rounded-2xl border border-border/50 bg-background-primary/50 px-4 py-6 pl-12 pr-36
10
- backdrop-blur-sm shadow-lg focus:outline-none focus:ring-2 focus:ring-primary/50
11
  text-lg transition-all duration-200 placeholder:text-text-secondary/50"
12
  />
13
  <svg
@@ -24,5 +29,5 @@ export default function SearchBar() {
24
  />
25
  </svg>
26
  </div>
27
- )
28
- }
 
1
+ "use client";
2
 
3
+ interface SearchBarProps {
4
+ onSearch: (query: string) => void;
5
+ }
6
+
7
+ export default function SearchBar({ onSearch }: SearchBarProps) {
8
  return (
9
  <div className="relative max-w-full mx-auto">
10
  <input
11
  type="text"
12
+ placeholder="搜尋 PlayGO AI..."
13
+ onChange={(e) => onSearch(e.target.value)}
14
  className="w-full rounded-2xl border border-border/50 bg-background-primary/50 px-4 py-6 pl-12 pr-36
15
+ backdrop-blur-sm shadow-lg focus:outline-none focus:ring-2 focus:ring-primary/50
16
  text-lg transition-all duration-200 placeholder:text-text-secondary/50"
17
  />
18
  <svg
 
29
  />
30
  </svg>
31
  </div>
32
+ );
33
+ }
app/(audiences)/components/subject-faceted-filter.tsx ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Check, PlusCircle } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { Button } from "@/components/ui/button";
8
+ import {
9
+ Command,
10
+ CommandEmpty,
11
+ CommandGroup,
12
+ CommandInput,
13
+ CommandItem,
14
+ CommandList,
15
+ CommandSeparator,
16
+ } from "@/components/ui/command";
17
+ import {
18
+ Popover,
19
+ PopoverContent,
20
+ PopoverTrigger,
21
+ } from "@/components/ui/popover";
22
+ import { Separator } from "@/components/ui/separator";
23
+
24
+ interface DataTableFacetedFilterProps {
25
+ title: string;
26
+ options: {
27
+ label: string;
28
+ value: string;
29
+ }[];
30
+ selectedValues: string[];
31
+ onSelectionChange: (values: string[]) => void;
32
+ }
33
+
34
+ export function DataTableFacetedFilter({
35
+ title,
36
+ options,
37
+ selectedValues,
38
+ onSelectionChange,
39
+ }: DataTableFacetedFilterProps) {
40
+ return (
41
+ <Popover>
42
+ <PopoverTrigger asChild>
43
+ <Button
44
+ variant="outline"
45
+ size="lg"
46
+ className="h-16 text-lg border-2 hover:bg-accent/50 min-w-[160px]"
47
+ >
48
+ <PlusCircle className="mr-3 h-6 w-6" />
49
+ {title}
50
+ {selectedValues?.length > 0 && (
51
+ <>
52
+ <Separator orientation="vertical" className="mx-4 h-8" />
53
+ <Badge
54
+ variant="secondary"
55
+ className="rounded-md px-3 py-1.5 text-base font-normal lg:hidden"
56
+ >
57
+ {selectedValues.length}
58
+ </Badge>
59
+ <div className="hidden space-x-2 lg:flex">
60
+ {selectedValues.length > 2 ? (
61
+ <Badge
62
+ variant="secondary"
63
+ className="rounded-md px-3 py-1.5 text-base font-normal"
64
+ >
65
+ 已選擇 {selectedValues.length} 項
66
+ </Badge>
67
+ ) : (
68
+ options
69
+ .filter((option) => selectedValues.includes(option.value))
70
+ .map((option) => (
71
+ <Badge
72
+ variant="secondary"
73
+ key={option.value}
74
+ className="rounded-md px-3 py-1.5 text-base font-normal"
75
+ >
76
+ {option.label}
77
+ </Badge>
78
+ ))
79
+ )}
80
+ </div>
81
+ </>
82
+ )}
83
+ </Button>
84
+ </PopoverTrigger>
85
+ <PopoverContent className="w-[320px] p-0" align="start">
86
+ <Command>
87
+ <CommandInput
88
+ placeholder={`搜尋${title}...`}
89
+ className="h-14 text-lg"
90
+ />
91
+ <CommandList>
92
+ <CommandEmpty>找不到結果</CommandEmpty>
93
+ <CommandGroup>
94
+ {options.map((option) => {
95
+ const isSelected = selectedValues.includes(option.value);
96
+ return (
97
+ <CommandItem
98
+ key={option.value}
99
+ onSelect={() => {
100
+ if (isSelected) {
101
+ onSelectionChange(
102
+ selectedValues.filter(
103
+ (value) => value !== option.value,
104
+ ),
105
+ );
106
+ } else {
107
+ onSelectionChange([...selectedValues, option.value]);
108
+ }
109
+ }}
110
+ className="p-4 text-lg"
111
+ >
112
+ <div
113
+ className={cn(
114
+ "mr-4 flex h-6 w-6 items-center justify-center rounded-sm border-2 border-primary",
115
+ isSelected
116
+ ? "bg-primary text-primary-foreground"
117
+ : "opacity-50 [&_svg]:invisible",
118
+ )}
119
+ >
120
+ <Check className="h-5 w-5" />
121
+ </div>
122
+ <span>{option.label}</span>
123
+ </CommandItem>
124
+ );
125
+ })}
126
+ </CommandGroup>
127
+ {selectedValues.length > 0 && (
128
+ <>
129
+ <CommandSeparator />
130
+ <CommandGroup>
131
+ <CommandItem
132
+ onSelect={() => onSelectionChange([])}
133
+ className="justify-center p-4 text-lg"
134
+ >
135
+ 清除篩選
136
+ </CommandItem>
137
+ </CommandGroup>
138
+ </>
139
+ )}
140
+ </CommandList>
141
+ </Command>
142
+ </PopoverContent>
143
+ </Popover>
144
+ );
145
+ }
app/(audiences)/components/subject-filter-view-options.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
4
+ import { Table } from "@tanstack/react-table";
5
+ import { Settings2 } from "lucide-react";
6
+
7
+ import { Button } from "@/components/ui/button";
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuCheckboxItem,
11
+ DropdownMenuContent,
12
+ DropdownMenuLabel,
13
+ DropdownMenuSeparator,
14
+ } from "@/components/ui/dropdown-menu";
15
+
16
+ interface DataTableViewOptionsProps<TData> {
17
+ table: Table<TData>;
18
+ }
19
+
20
+ export function DataTableViewOptions<TData>({
21
+ table,
22
+ }: DataTableViewOptionsProps<TData>) {
23
+ return (
24
+ <DropdownMenu>
25
+ <DropdownMenuTrigger asChild>
26
+ <Button
27
+ variant="outline"
28
+ size="sm"
29
+ className="ml-auto hidden h-8 lg:flex"
30
+ >
31
+ <Settings2 />
32
+ View
33
+ </Button>
34
+ </DropdownMenuTrigger>
35
+ <DropdownMenuContent align="end" className="w-[150px]">
36
+ <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
37
+ <DropdownMenuSeparator />
38
+ {table
39
+ .getAllColumns()
40
+ .filter(
41
+ (column) =>
42
+ typeof column.accessorFn !== "undefined" && column.getCanHide(),
43
+ )
44
+ .map((column) => {
45
+ return (
46
+ <DropdownMenuCheckboxItem
47
+ key={column.id}
48
+ className="capitalize"
49
+ checked={column.getIsVisible()}
50
+ onCheckedChange={(value) => column.toggleVisibility(!!value)}
51
+ >
52
+ {column.id}
53
+ </DropdownMenuCheckboxItem>
54
+ );
55
+ })}
56
+ </DropdownMenuContent>
57
+ </DropdownMenu>
58
+ );
59
+ }
app/(audiences)/components/subject-filter.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { categories, subjects } from "@/app/(audiences)/for-students/data/data";
5
+ import { DataTableFacetedFilter } from "@/app/(audiences)/components/subject-faceted-filter";
6
+ import { Button } from "@/components/ui/button";
7
+ import { X } from "lucide-react";
8
+
9
+ interface SubjectFilterProps {
10
+ selectedCategories: string[];
11
+ setSelectedCategories: (categories: string[]) => void;
12
+ selectedSubjects: string[];
13
+ setSelectedSubjects: (subjects: string[]) => void;
14
+ }
15
+
16
+ export function SubjectFilter({
17
+ selectedCategories,
18
+ setSelectedCategories,
19
+ selectedSubjects,
20
+ setSelectedSubjects,
21
+ }: SubjectFilterProps) {
22
+ const isFiltered =
23
+ selectedCategories.length > 0 || selectedSubjects.length > 0;
24
+
25
+ const handleReset = () => {
26
+ setSelectedCategories([]);
27
+ setSelectedSubjects([]);
28
+ };
29
+
30
+ return (
31
+ <div className="flex items-center justify-between">
32
+ <div className="flex flex-1 items-center space-x-6">
33
+ <DataTableFacetedFilter
34
+ title="科目"
35
+ options={subjects}
36
+ selectedValues={selectedSubjects}
37
+ onSelectionChange={setSelectedSubjects}
38
+ />
39
+ <DataTableFacetedFilter
40
+ title="類別"
41
+ options={categories}
42
+ selectedValues={selectedCategories}
43
+ onSelectionChange={setSelectedCategories}
44
+ />
45
+ {isFiltered && (
46
+ <Button
47
+ variant="ghost"
48
+ size="lg"
49
+ onClick={handleReset}
50
+ className="h-16 px-6 text-lg"
51
+ >
52
+ 重設
53
+ <X className="ml-3 h-6 w-6" />
54
+ </Button>
55
+ )}
56
+ </div>
57
+ </div>
58
+ );
59
+ }
app/(audiences)/for-students/data/chatbots.ts ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ChatBot {
2
+ id: string;
3
+ title: string;
4
+ description: string;
5
+ subject: string;
6
+ icon: string;
7
+ category: string;
8
+ popular?: boolean;
9
+ trending?: boolean;
10
+ }
11
+
12
+ export const CHATBOTS: ChatBot[] = [
13
+ // 精選 (最多人用)
14
+ {
15
+ id: "1",
16
+ title: "Code Tutor",
17
+ description: "Let's code together! I'm Khanmigo Lite, by Khan Academy.",
18
+ subject: "電腦科學",
19
+ category: "教育",
20
+ icon: "code-tutor.webp",
21
+ popular: true,
22
+ },
23
+ {
24
+ id: "2",
25
+ title: "Math Solver",
26
+ description: "數學解題助手,從基礎數學到高等微積分都能協助理解和解題。",
27
+ subject: "數學",
28
+ category: "教育",
29
+ icon: "code-tutor.webp",
30
+ popular: true,
31
+ },
32
+ {
33
+ id: "3",
34
+ title: "Essay Writer",
35
+ description: "協助改進寫作技巧,提供建議和修改意見,適用於各種文體。",
36
+ subject: "國語文",
37
+ category: "寫作",
38
+ icon: "code-tutor.webp",
39
+ popular: true,
40
+ },
41
+ {
42
+ id: "4",
43
+ title: "English Master",
44
+ description: "全方位英語學習助手,提供聽說讀寫全面訓練。",
45
+ subject: "英文",
46
+ category: "語言",
47
+ icon: "code-tutor.webp",
48
+ popular: true,
49
+ },
50
+
51
+ // 熱門 (近期熱門)
52
+ {
53
+ id: "5",
54
+ title: "Math Practice",
55
+ description: "針對各年級數學題目練習,提供詳細解答和步驟說明。",
56
+ subject: "數學",
57
+ category: "教育",
58
+ icon: "code-tutor.webp",
59
+ trending: true,
60
+ },
61
+ {
62
+ id: "6",
63
+ title: "English Conversation",
64
+ description: "實用英語對話練習,提升口語和聽力能力。",
65
+ subject: "英文",
66
+ category: "語言",
67
+ icon: "code-tutor.webp",
68
+ trending: true,
69
+ },
70
+ {
71
+ id: "7",
72
+ title: "Physics Tutor",
73
+ description: "物理概念解說和題目解析,讓物理學習更輕鬆。",
74
+ subject: "自然科學",
75
+ category: "教育",
76
+ icon: "code-tutor.webp",
77
+ trending: true,
78
+ },
79
+ {
80
+ id: "8",
81
+ title: "Chemistry Lab",
82
+ description: "虛擬化學實驗室,安全地探索化學反應和概念。",
83
+ subject: "自然科學",
84
+ category: "教育",
85
+ icon: "code-tutor.webp",
86
+ trending: true,
87
+ },
88
+
89
+ // 家教、數學、英文相關
90
+ {
91
+ id: "9",
92
+ title: "Personal Tutor",
93
+ description: "個人化學習規劃和指導,適應每個學生的學習步調。",
94
+ subject: "教學",
95
+ category: "教育",
96
+ icon: "code-tutor.webp",
97
+ },
98
+ {
99
+ id: "10",
100
+ title: "Math Concepts",
101
+ description: "深入淺出講解數學概念,建立紮實的數學基礎。",
102
+ subject: "數學",
103
+ category: "教育",
104
+ icon: "code-tutor.webp",
105
+ },
106
+ {
107
+ id: "11",
108
+ title: "English Grammar",
109
+ description: "系統性學習英語文法,從基礎到進階全面掌握。",
110
+ subject: "英文",
111
+ category: "語言",
112
+ icon: "code-tutor.webp",
113
+ },
114
+ {
115
+ id: "12",
116
+ title: "English Writing",
117
+ description: "英文寫作指導,從句子到文章的全方位訓練。",
118
+ subject: "英文",
119
+ category: "語言",
120
+ icon: "code-tutor.webp",
121
+ },
122
+ {
123
+ id: "13",
124
+ title: "Math Problems",
125
+ description: "豐富的數學題庫,配合詳細解說和練習。",
126
+ subject: "數學",
127
+ category: "教育",
128
+ icon: "code-tutor.webp",
129
+ },
130
+ {
131
+ id: "14",
132
+ title: "TOEIC Prep",
133
+ description: "針對多益考試的專業培訓和模擬測驗。",
134
+ subject: "英文",
135
+ category: "語言",
136
+ icon: "code-tutor.webp",
137
+ },
138
+ ];
139
+
140
+ export const getPopularBots = () => CHATBOTS.filter((bot) => bot.popular);
141
+ export const getTrendingBots = () => CHATBOTS.filter((bot) => bot.trending);
142
+ export const getAllBots = () => CHATBOTS;
app/(audiences)/for-students/data/data.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const categories = [
2
+ {
3
+ value: "熱門精選",
4
+ label: "熱門精選",
5
+ },
6
+ {
7
+ value: "寫作",
8
+ label: "寫作",
9
+ },
10
+ {
11
+ value: "生產力",
12
+ label: "生產力",
13
+ },
14
+ {
15
+ value: "研究與分析",
16
+ label: "研究與分析",
17
+ },
18
+ {
19
+ value: "教育",
20
+ label: "教育",
21
+ },
22
+ ];
23
+
24
+ export const subjects = [
25
+ {
26
+ value: "國語文",
27
+ label: "國語文",
28
+ },
29
+ {
30
+ value: "英文",
31
+ label: "英文",
32
+ },
33
+ {
34
+ value: "數學",
35
+ label: "數學",
36
+ },
37
+ {
38
+ value: "自然科學",
39
+ label: "自然科學",
40
+ },
41
+ {
42
+ value: "社會科學",
43
+ label: "社會科學",
44
+ },
45
+ {
46
+ value: "電腦科學",
47
+ label: "電腦科學",
48
+ },
49
+ ];
app/{for-students → (audiences)/for-students}/page.tsx RENAMED
@@ -1,36 +1,62 @@
1
- import { Suspense } from 'react'
2
- import ChatBotGrid from '@/components/custom/ChatBotGrid'
3
- import SubjectFilter from '@/components/custom/SubjectFilter'
4
- import SearchBar from '@/components/custom/SearchBar'
5
- import ChatBotGridSkeleton from '@/components/custom/ChatBotGridSkeleton'
 
 
 
 
6
 
7
  export default function ForStudentsPage() {
 
 
 
 
 
 
 
 
 
 
8
  return (
9
  <main className="min-h-screen bg-background-primary">
10
  {/* Hero Section */}
11
  <section className="mb-12 pt-16 text-center">
12
  <h1 className="mb-4 text-5xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
13
- GPT
14
  </h1>
15
  <p className="mx-auto max-w-2xl text-lg text-text-secondary">
16
- 探索並建立結合指令、額外知識庫和任何技能組合的 ChatGPT 自訂版本。
17
  </p>
18
  </section>
19
 
20
  {/* Search and Filter Section */}
21
  <div className="container mx-auto px-4">
22
  <div className="mb-8">
23
- <SearchBar />
24
  </div>
25
  <div className="mb-12">
26
- <SubjectFilter />
 
 
 
 
 
27
  </div>
28
 
29
  {/* Chatbots Grid */}
30
  <Suspense fallback={<ChatBotGridSkeleton />}>
31
- <ChatBotGrid />
 
 
 
 
 
 
 
32
  </Suspense>
33
  </div>
34
  </main>
35
- )
36
- }
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Suspense } from "react";
5
+ import ChatBotGrid from "@/app/(audiences)/components/chat-bot-grid";
6
+ import { SubjectFilter } from "@/app/(audiences)/components/subject-filter";
7
+ import SearchBar from "@/app/(audiences)/components/search-bar";
8
+ import ChatBotGridSkeleton from "@/app/(audiences)/components/chat-bot-grid-skeleton";
9
+ import { getPopularBots, getTrendingBots, getAllBots } from "./data/chatbots";
10
 
11
  export default function ForStudentsPage() {
12
+ const [selectedCategories, setSelectedCategories] = React.useState<string[]>(
13
+ [],
14
+ );
15
+ const [selectedSubjects, setSelectedSubjects] = React.useState<string[]>([]);
16
+ const [searchQuery, setSearchQuery] = React.useState<string>("");
17
+
18
+ const handleSearch = (query: string) => {
19
+ setSearchQuery(query);
20
+ };
21
+
22
  return (
23
  <main className="min-h-screen bg-background-primary">
24
  {/* Hero Section */}
25
  <section className="mb-12 pt-16 text-center">
26
  <h1 className="mb-4 text-5xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
27
+ PlayGO for Students
28
  </h1>
29
  <p className="mx-auto max-w-2xl text-lg text-text-secondary">
30
+ 探索適合學習的 AI 聊天機器人,提升學習效率!
31
  </p>
32
  </section>
33
 
34
  {/* Search and Filter Section */}
35
  <div className="container mx-auto px-4">
36
  <div className="mb-8">
37
+ <SearchBar onSearch={handleSearch} />
38
  </div>
39
  <div className="mb-12">
40
+ <SubjectFilter
41
+ selectedCategories={selectedCategories}
42
+ setSelectedCategories={setSelectedCategories}
43
+ selectedSubjects={selectedSubjects}
44
+ setSelectedSubjects={setSelectedSubjects}
45
+ />
46
  </div>
47
 
48
  {/* Chatbots Grid */}
49
  <Suspense fallback={<ChatBotGridSkeleton />}>
50
+ <ChatBotGrid
51
+ selectedCategories={selectedCategories}
52
+ selectedSubjects={selectedSubjects}
53
+ searchQuery={searchQuery}
54
+ getPopularBots={getPopularBots}
55
+ getTrendingBots={getTrendingBots}
56
+ getAllBots={getAllBots}
57
+ />
58
  </Suspense>
59
  </div>
60
  </main>
61
+ );
62
+ }
app/(audiences)/for-teachers/data/chatbots.ts ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChatBot } from "@/app/(audiences)/for-students/data/chatbots";
2
+
3
+ export const CHATBOTS: ChatBot[] = [
4
+ // 精選 (最多人用)
5
+ {
6
+ id: "1",
7
+ title: "Lesson Planner",
8
+ description: "智能課程規劃助手,協助教師設計有效的教學計劃。",
9
+ subject: "教學",
10
+ category: "教育",
11
+ icon: "lesson-planner.webp",
12
+ popular: true,
13
+ },
14
+ {
15
+ id: "2",
16
+ title: "Assignment Creator",
17
+ description: "自動生成作業和測驗,節省備課時間。",
18
+ subject: "教學",
19
+ category: "教育",
20
+ icon: "assignment-creator.webp",
21
+ popular: true,
22
+ },
23
+ {
24
+ id: "3",
25
+ title: "Grading Assistant",
26
+ description: "智能評分助手,提供客觀的評分建議和回饋。",
27
+ subject: "評量",
28
+ category: "教育",
29
+ icon: "grading-assistant.webp",
30
+ popular: true,
31
+ },
32
+ {
33
+ id: "4",
34
+ title: "Student Analytics",
35
+ description: "學生學習數據分析,掌握學習成效。",
36
+ subject: "分析",
37
+ category: "研究與分析",
38
+ icon: "student-analytics.webp",
39
+ popular: true,
40
+ },
41
+
42
+ // 熱門 (近期熱門)
43
+ {
44
+ id: "5",
45
+ title: "Content Creator",
46
+ description: "教材內容生成助手,快速製作教學素材。",
47
+ subject: "教學",
48
+ category: "生產力",
49
+ icon: "content-creator.webp",
50
+ trending: true,
51
+ },
52
+ {
53
+ id: "6",
54
+ title: "Classroom Manager",
55
+ description: "課堂管理助手,提升教學效率。",
56
+ subject: "管理",
57
+ category: "教育",
58
+ icon: "classroom-manager.webp",
59
+ trending: true,
60
+ },
61
+ // Add more teacher-specific chatbots...
62
+ ];
63
+
64
+ export const getPopularBots = () => CHATBOTS.filter((bot) => bot.popular);
65
+ export const getTrendingBots = () => CHATBOTS.filter((bot) => bot.trending);
66
+ export const getAllBots = () => CHATBOTS;
app/(audiences)/for-teachers/page.tsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Suspense } from "react";
5
+ import ChatBotGrid from "@/app/(audiences)/components/chat-bot-grid";
6
+ import { SubjectFilter } from "@/app/(audiences)/components/subject-filter";
7
+ import SearchBar from "@/app/(audiences)/components/search-bar";
8
+ import ChatBotGridSkeleton from "@/app/(audiences)/components/chat-bot-grid-skeleton";
9
+ import { getPopularBots, getTrendingBots, getAllBots } from "./data/chatbots";
10
+
11
+ export default function ForTeachersPage() {
12
+ const [selectedCategories, setSelectedCategories] = React.useState<string[]>(
13
+ [],
14
+ );
15
+ const [selectedSubjects, setSelectedSubjects] = React.useState<string[]>([]);
16
+ const [searchQuery, setSearchQuery] = React.useState<string>("");
17
+
18
+ const handleSearch = (query: string) => {
19
+ setSearchQuery(query);
20
+ };
21
+
22
+ return (
23
+ <main className="min-h-screen bg-background-primary">
24
+ {/* Hero Section */}
25
+ <section className="mb-12 pt-16 text-center">
26
+ <h1 className="mb-4 text-5xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
27
+ PlayGO for Teachers
28
+ </h1>
29
+ <p className="mx-auto max-w-2xl text-lg text-text-secondary">
30
+ 探索適合教學的 AI 聊天機器人,提升教學效率!
31
+ </p>
32
+ </section>
33
+
34
+ {/* Search and Filter Section */}
35
+ <div className="container mx-auto px-4">
36
+ <div className="mb-8">
37
+ <SearchBar onSearch={handleSearch} />
38
+ </div>
39
+ <div className="mb-12">
40
+ <SubjectFilter
41
+ selectedCategories={selectedCategories}
42
+ setSelectedCategories={setSelectedCategories}
43
+ selectedSubjects={selectedSubjects}
44
+ setSelectedSubjects={setSelectedSubjects}
45
+ />
46
+ </div>
47
+
48
+ {/* Chatbots Grid */}
49
+ <Suspense fallback={<ChatBotGridSkeleton />}>
50
+ <ChatBotGrid
51
+ selectedCategories={selectedCategories}
52
+ selectedSubjects={selectedSubjects}
53
+ searchQuery={searchQuery}
54
+ getPopularBots={getPopularBots}
55
+ getTrendingBots={getTrendingBots}
56
+ getAllBots={getAllBots}
57
+ />
58
+ </Suspense>
59
+ </div>
60
+ </main>
61
+ );
62
+ }
app/components/chatbot/chat-bot-card.tsx ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ // Move the existing ChatBotCard component here with no changes needed
2
+ // This is already well-structured as a reusable component
app/components/chatbot/chat-bot-grid.tsx ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import ChatBotCard from "@/app/(audiences)/components/chat-bot-card";
5
+ import { ChatBot } from "@/types/chatbot";
6
+
7
+ interface SectionProps {
8
+ title: string;
9
+ subtitle: string;
10
+ bots: ChatBot[];
11
+ columns?: number;
12
+ }
13
+
14
+ function Section({ title, subtitle, bots, columns = 4 }: SectionProps) {
15
+ if (bots.length === 0) return null;
16
+
17
+ return (
18
+ <div className="mb-16">
19
+ <div className="mb-8">
20
+ <h2 className="text-2xl font-bold text-text-primary mb-2">{title}</h2>
21
+ <p className="text-text-secondary">{subtitle}</p>
22
+ </div>
23
+ <div className={`grid gap-4 sm:grid-cols-2 lg:grid-cols-${columns}`}>
24
+ {bots.map((chatbot) => (
25
+ <ChatBotCard key={chatbot.id} chatbot={chatbot} />
26
+ ))}
27
+ </div>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ interface ChatBotGridProps {
33
+ selectedCategories: string[];
34
+ selectedSubjects: string[];
35
+ searchQuery: string;
36
+ getPopularBots: () => ChatBot[];
37
+ getTrendingBots: () => ChatBot[];
38
+ getAllBots: () => ChatBot[];
39
+ }
40
+
41
+ export default function ChatBotGrid({
42
+ selectedCategories,
43
+ selectedSubjects,
44
+ searchQuery,
45
+ getPopularBots,
46
+ getTrendingBots,
47
+ getAllBots,
48
+ }: ChatBotGridProps) {
49
+ const [filteredPopularBots, setFilteredPopularBots] =
50
+ useState<ChatBot[]>(getPopularBots());
51
+ const [filteredTrendingBots, setFilteredTrendingBots] =
52
+ useState<ChatBot[]>(getTrendingBots());
53
+ const [filteredAllBots, setFilteredAllBots] =
54
+ useState<ChatBot[]>(getAllBots());
55
+
56
+ useEffect(() => {
57
+ const filterBots = (bots: ChatBot[]) => {
58
+ let filtered = bots;
59
+
60
+ if (searchQuery) {
61
+ const query = searchQuery.toLowerCase();
62
+ filtered = filtered.filter(
63
+ (bot) =>
64
+ bot.title.toLowerCase().includes(query) ||
65
+ bot.description.toLowerCase().includes(query) ||
66
+ bot.subject.toLowerCase().includes(query) ||
67
+ bot.category.toLowerCase().includes(query),
68
+ );
69
+ }
70
+
71
+ if (selectedCategories.length > 0) {
72
+ filtered = filtered.filter(
73
+ (bot) => bot.category && selectedCategories.includes(bot.category),
74
+ );
75
+ }
76
+
77
+ if (selectedSubjects.length > 0) {
78
+ filtered = filtered.filter((bot) =>
79
+ selectedSubjects.includes(bot.subject),
80
+ );
81
+ }
82
+
83
+ return filtered;
84
+ };
85
+
86
+ setFilteredPopularBots(filterBots(getPopularBots()));
87
+ setFilteredTrendingBots(filterBots(getTrendingBots()));
88
+ setFilteredAllBots(filterBots(getAllBots()));
89
+ }, [
90
+ selectedCategories,
91
+ selectedSubjects,
92
+ searchQuery,
93
+ getPopularBots,
94
+ getTrendingBots,
95
+ getAllBots,
96
+ ]);
97
+
98
+ const hasFiltersOrSearch =
99
+ selectedCategories.length > 0 || selectedSubjects.length > 0 || searchQuery;
100
+
101
+ if (hasFiltersOrSearch) {
102
+ return (
103
+ <Section
104
+ title="搜尋結果"
105
+ subtitle={`符合 ${searchQuery ? '"' + searchQuery + '" ' : ""}的工具`}
106
+ bots={filteredAllBots}
107
+ columns={4}
108
+ />
109
+ );
110
+ }
111
+
112
+ const tutorBots = filteredAllBots.filter((bot) => bot.category === "教育");
113
+ const teachingBots = filteredAllBots.filter((bot) => bot.subject === "教學");
114
+ const assessmentBots = filteredAllBots.filter(
115
+ (bot) => bot.subject === "評量",
116
+ );
117
+
118
+ return (
119
+ <div className="space-y-8">
120
+ <Section
121
+ title="精選"
122
+ subtitle="歷來最多人使用的工具"
123
+ bots={filteredPopularBots}
124
+ columns={4}
125
+ />
126
+ <Section
127
+ title="熱門"
128
+ subtitle="近期最多人使用的工具"
129
+ bots={filteredTrendingBots}
130
+ columns={4}
131
+ />
132
+ <Section
133
+ title="教學輔助"
134
+ subtitle="教學規劃與課程設計工具"
135
+ bots={teachingBots}
136
+ columns={4}
137
+ />
138
+ <Section
139
+ title="評量工具"
140
+ subtitle="作業與評量相關工具"
141
+ bots={assessmentBots}
142
+ columns={4}
143
+ />
144
+ <Section
145
+ title="教育資源"
146
+ subtitle="其他教育相關工具"
147
+ bots={tutorBots}
148
+ columns={4}
149
+ />
150
+ </div>
151
+ );
152
+ }
components/custom/LandingPageChatBot.tsx → app/components/landing-page-chat-bot.tsx RENAMED
@@ -1,97 +1,111 @@
1
- 'use client'
2
 
3
- import { useState, useRef, useEffect } from 'react'
4
- import { useChat } from 'ai/react'
5
  import {
6
  AcademicCapIcon,
7
  ClockIcon,
8
  BookOpenIcon,
9
  LightBulbIcon,
10
  BriefcaseIcon,
11
- } from '@heroicons/react/24/outline'
12
 
13
  type MessageWithLoading = {
14
  content: string;
15
  role: string;
16
  isStreaming?: boolean;
17
- }
18
 
19
  const EXAMPLE_PROMPTS = [
20
  {
21
  title: "教案規劃",
22
  prompt: "我想規劃一堂關於數學的課程",
23
- icon: AcademicCapIcon
24
  },
25
  {
26
  title: "英語對話",
27
  prompt: "我想練習英語對話",
28
- icon: ClockIcon
29
  },
30
  {
31
  title: "作業批改",
32
  prompt: "我想批改學生的作文作業",
33
- icon: BookOpenIcon
34
  },
35
  {
36
  title: "數學解題",
37
  prompt: "我想解數學題目",
38
- icon: LightBulbIcon
39
  },
40
  {
41
  title: "我想學程式",
42
  prompt: "我想學習如何寫程式",
43
- icon: BriefcaseIcon
44
- }
45
- ]
46
 
47
  export default function LandingPageChatBot() {
48
- const { messages: rawMessages, input, handleInputChange, handleSubmit, isLoading, append } = useChat({
49
- api: '/api/landing_page_chat',
50
- streamProtocol: 'data',
 
 
 
 
 
 
 
51
  onError: (error) => {
52
- console.error('Chat error:', error);
53
  },
54
  onFinish: (message) => {
55
- console.log('Chat finished:', message);
56
- setMessages(prev => prev.map(msg => ({...msg, isStreaming: false})))
 
 
57
  },
58
  keepLastMessageOnError: true,
59
- })
60
 
61
- const chatContainerRef = useRef<HTMLDivElement>(null)
62
- const [hasInteracted, setHasInteracted] = useState(false)
63
- const [messages, setMessages] = useState<MessageWithLoading[]>([])
64
 
65
  useEffect(() => {
66
- setMessages(rawMessages.map((msg, index) => ({
67
- ...msg,
68
- isStreaming: isLoading && index === rawMessages.length - 1 && msg.role === 'assistant'
69
- })))
70
- }, [rawMessages, isLoading])
 
 
 
 
 
71
 
72
  const scrollToBottom = () => {
73
  if (chatContainerRef.current) {
74
- const { scrollHeight, clientHeight } = chatContainerRef.current
75
- chatContainerRef.current.scrollTop = scrollHeight - clientHeight
76
  }
77
- }
78
 
79
  useEffect(() => {
80
- scrollToBottom()
81
- }, [messages])
82
 
83
  const sendMessage = async (text: string) => {
84
- setHasInteracted(true)
85
  await append({
86
  content: text,
87
- role: 'user',
88
- })
89
- }
90
 
91
  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
92
- setHasInteracted(true)
93
- handleSubmit(e)
94
- }
95
 
96
  return (
97
  <section className="w-full h-[1000px] bg-gradient-to-b from-background-secondary to-background-primary flex items-center justify-center px-4">
@@ -132,12 +146,12 @@ export default function LandingPageChatBot() {
132
  viewBox="0 0 24 24"
133
  strokeWidth="1.5"
134
  stroke="currentColor"
135
- className={`size-6 ${isLoading ? 'animate-pulse' : ''}`}
136
  >
137
  <path
138
  strokeLinecap="round"
139
  strokeLinejoin="round"
140
- d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
141
  />
142
  </svg>
143
  </button>
@@ -168,7 +182,11 @@ export default function LandingPageChatBot() {
168
  viewBox="0 0 24 24"
169
  strokeWidth={2}
170
  >
171
- <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
 
 
 
 
172
  </svg>
173
  </div>
174
  </button>
@@ -179,7 +197,9 @@ export default function LandingPageChatBot() {
179
  ) : (
180
  <div className="bg-background-primary/50 backdrop-blur-sm rounded-2xl shadow-lg border border-border/50 overflow-hidden w-full h-[700px] flex flex-col">
181
  <div className="p-6 border-b border-border/50 bg-background-secondary/30">
182
- <h2 className="text-xl font-semibold text-text-primary">PlayGO 導覽員</h2>
 
 
183
  </div>
184
 
185
  <div
@@ -189,13 +209,13 @@ export default function LandingPageChatBot() {
189
  {messages.map((message, index) => (
190
  <div
191
  key={index}
192
- className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
193
  >
194
  <div
195
  className={`max-w-[80%] p-4 rounded-2xl shadow-sm ${
196
- message.role === 'user'
197
- ? 'bg-primary text-white rounded-br-none'
198
- : 'bg-background-secondary/50 text-text-primary rounded-bl-none'
199
  }`}
200
  >
201
  {message.content}
@@ -214,7 +234,9 @@ export default function LandingPageChatBot() {
214
  type="text"
215
  value={input}
216
  onChange={handleInputChange}
217
- placeholder={isLoading ? "AI is thinking..." : "Type your message..."}
 
 
218
  disabled={isLoading}
219
  className="w-full p-4 pr-32 rounded-xl border border-border/50 bg-background-primary/50
220
  backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-primary/50
@@ -228,18 +250,18 @@ export default function LandingPageChatBot() {
228
  shadow-md hover:shadow-lg active:scale-95 disabled:opacity-50
229
  disabled:hover:bg-primary disabled:cursor-not-allowed"
230
  >
231
- <svg
232
- xmlns="http://www.w3.org/2000/svg"
233
- fill="none"
234
- viewBox="0 0 24 24"
235
- strokeWidth="1.5"
236
- stroke="currentColor"
237
- className={`size-5 ${isLoading ? 'animate-pulse' : ''}`}
238
  >
239
- <path
240
- strokeLinecap="round"
241
- strokeLinejoin="round"
242
- d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
243
  />
244
  </svg>
245
  </button>
@@ -250,5 +272,5 @@ export default function LandingPageChatBot() {
250
  )}
251
  </div>
252
  </section>
253
- )
254
- }
 
1
+ "use client";
2
 
3
+ import { useState, useRef, useEffect } from "react";
4
+ import { useChat } from "ai/react";
5
  import {
6
  AcademicCapIcon,
7
  ClockIcon,
8
  BookOpenIcon,
9
  LightBulbIcon,
10
  BriefcaseIcon,
11
+ } from "@heroicons/react/24/outline";
12
 
13
  type MessageWithLoading = {
14
  content: string;
15
  role: string;
16
  isStreaming?: boolean;
17
+ };
18
 
19
  const EXAMPLE_PROMPTS = [
20
  {
21
  title: "教案規劃",
22
  prompt: "我想規劃一堂關於數學的課程",
23
+ icon: AcademicCapIcon,
24
  },
25
  {
26
  title: "英語對話",
27
  prompt: "我想練習英語對話",
28
+ icon: ClockIcon,
29
  },
30
  {
31
  title: "作業批改",
32
  prompt: "我想批改學生的作文作業",
33
+ icon: BookOpenIcon,
34
  },
35
  {
36
  title: "數學解題",
37
  prompt: "我想解數學題目",
38
+ icon: LightBulbIcon,
39
  },
40
  {
41
  title: "我想學程式",
42
  prompt: "我想學習如何寫程式",
43
+ icon: BriefcaseIcon,
44
+ },
45
+ ];
46
 
47
  export default function LandingPageChatBot() {
48
+ const {
49
+ messages: rawMessages,
50
+ input,
51
+ handleInputChange,
52
+ handleSubmit,
53
+ isLoading,
54
+ append,
55
+ } = useChat({
56
+ api: "/api/landing_page_chat",
57
+ streamProtocol: "data",
58
  onError: (error) => {
59
+ console.error("Chat error:", error);
60
  },
61
  onFinish: (message) => {
62
+ console.log("Chat finished:", message);
63
+ setMessages((prev) =>
64
+ prev.map((msg) => ({ ...msg, isStreaming: false })),
65
+ );
66
  },
67
  keepLastMessageOnError: true,
68
+ });
69
 
70
+ const chatContainerRef = useRef<HTMLDivElement>(null);
71
+ const [hasInteracted, setHasInteracted] = useState(false);
72
+ const [messages, setMessages] = useState<MessageWithLoading[]>([]);
73
 
74
  useEffect(() => {
75
+ setMessages(
76
+ rawMessages.map((msg, index) => ({
77
+ ...msg,
78
+ isStreaming:
79
+ isLoading &&
80
+ index === rawMessages.length - 1 &&
81
+ msg.role === "assistant",
82
+ })),
83
+ );
84
+ }, [rawMessages, isLoading]);
85
 
86
  const scrollToBottom = () => {
87
  if (chatContainerRef.current) {
88
+ const { scrollHeight, clientHeight } = chatContainerRef.current;
89
+ chatContainerRef.current.scrollTop = scrollHeight - clientHeight;
90
  }
91
+ };
92
 
93
  useEffect(() => {
94
+ scrollToBottom();
95
+ }, [messages]);
96
 
97
  const sendMessage = async (text: string) => {
98
+ setHasInteracted(true);
99
  await append({
100
  content: text,
101
+ role: "user",
102
+ });
103
+ };
104
 
105
  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
106
+ setHasInteracted(true);
107
+ handleSubmit(e);
108
+ };
109
 
110
  return (
111
  <section className="w-full h-[1000px] bg-gradient-to-b from-background-secondary to-background-primary flex items-center justify-center px-4">
 
146
  viewBox="0 0 24 24"
147
  strokeWidth="1.5"
148
  stroke="currentColor"
149
+ className={`size-6 ${isLoading ? "animate-pulse" : ""}`}
150
  >
151
  <path
152
  strokeLinecap="round"
153
  strokeLinejoin="round"
154
+ d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
155
  />
156
  </svg>
157
  </button>
 
182
  viewBox="0 0 24 24"
183
  strokeWidth={2}
184
  >
185
+ <path
186
+ strokeLinecap="round"
187
+ strokeLinejoin="round"
188
+ d="M9 5l7 7-7 7"
189
+ />
190
  </svg>
191
  </div>
192
  </button>
 
197
  ) : (
198
  <div className="bg-background-primary/50 backdrop-blur-sm rounded-2xl shadow-lg border border-border/50 overflow-hidden w-full h-[700px] flex flex-col">
199
  <div className="p-6 border-b border-border/50 bg-background-secondary/30">
200
+ <h2 className="text-xl font-semibold text-text-primary">
201
+ PlayGO 導覽員
202
+ </h2>
203
  </div>
204
 
205
  <div
 
209
  {messages.map((message, index) => (
210
  <div
211
  key={index}
212
+ className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}
213
  >
214
  <div
215
  className={`max-w-[80%] p-4 rounded-2xl shadow-sm ${
216
+ message.role === "user"
217
+ ? "bg-primary text-white rounded-br-none"
218
+ : "bg-background-secondary/50 text-text-primary rounded-bl-none"
219
  }`}
220
  >
221
  {message.content}
 
234
  type="text"
235
  value={input}
236
  onChange={handleInputChange}
237
+ placeholder={
238
+ isLoading ? "AI is thinking..." : "Type your message..."
239
+ }
240
  disabled={isLoading}
241
  className="w-full p-4 pr-32 rounded-xl border border-border/50 bg-background-primary/50
242
  backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-primary/50
 
250
  shadow-md hover:shadow-lg active:scale-95 disabled:opacity-50
251
  disabled:hover:bg-primary disabled:cursor-not-allowed"
252
  >
253
+ <svg
254
+ xmlns="http://www.w3.org/2000/svg"
255
+ fill="none"
256
+ viewBox="0 0 24 24"
257
+ strokeWidth="1.5"
258
+ stroke="currentColor"
259
+ className={`size-5 ${isLoading ? "animate-pulse" : ""}`}
260
  >
261
+ <path
262
+ strokeLinecap="round"
263
+ strokeLinejoin="round"
264
+ d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
265
  />
266
  </svg>
267
  </button>
 
272
  )}
273
  </div>
274
  </section>
275
+ );
276
+ }
app/components/theme-provider.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ThemeProvider as NextThemesProvider } from "next-themes";
5
+
6
+ /**
7
+ * Custom ThemeProvider component that wraps next-themes provider
8
+ * with hydration mismatch prevention.
9
+ *
10
+ * This component ensures theme switching functionality works correctly
11
+ * by only rendering after initial client-side mount to prevent
12
+ * hydration mismatches between server and client rendering.
13
+ *
14
+ * @param children - Child components to be wrapped by the theme provider
15
+ * @param props - Additional props passed to NextThemesProvider
16
+ */
17
+ export function ThemeProvider({
18
+ children,
19
+ ...props
20
+ }: React.ComponentProps<typeof NextThemesProvider>) {
21
+ // Track whether component has mounted on client-side
22
+ const [mounted, setMounted] = React.useState(false);
23
+
24
+ // Set mounted state to true after initial client-side render
25
+ // This ensures we only render content after hydration is complete
26
+ React.useEffect(() => {
27
+ setMounted(true);
28
+ }, []);
29
+
30
+ // Return null on server-side and initial client-side render
31
+ // to prevent hydration mismatch with theme preferences
32
+ if (!mounted) {
33
+ return null;
34
+ }
35
+
36
+ // Once mounted, render the theme provider with children
37
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
38
+ }
app/components/top-nav.tsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Moon, Sun } from "lucide-react";
5
+ import { useTheme } from "next-themes";
6
+ import Link from "next/link";
7
+ import { Button } from "@/components/ui/button";
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuTrigger,
13
+ } from "@/components/ui/dropdown-menu";
14
+
15
+ /**
16
+ * TopNav - A responsive navigation header component with theme switching functionality
17
+ * Features:
18
+ * - Colorful brand logo/text
19
+ * - Navigation links for teachers and students
20
+ * - Theme switcher (light/dark/system) with animated icons
21
+ * - Client-side hydration handling
22
+ */
23
+ export function TopNav() {
24
+ const { setTheme } = useTheme();
25
+ // Track component mounting to prevent hydration mismatch
26
+ const [mounted, setMounted] = React.useState(false);
27
+
28
+ // Enable theme switching only after client-side hydration
29
+ React.useEffect(() => {
30
+ setMounted(true);
31
+ }, []);
32
+
33
+ // Pre-hydration render - shows a simplified version without theme toggle
34
+ if (!mounted) {
35
+ return (
36
+ <header className="fixed w-full bg-background-secondary/80 backdrop-blur-sm border-b border-border z-50">
37
+ <div className="container mx-auto px-4 py-4 flex items-center">
38
+ <Link href="/" className="text-3xl font-bold">
39
+ <span className="text-[#FF6B6B]">P</span>
40
+ <span className="text-[#4ECDC4]">l</span>
41
+ <span className="text-[#45B7D1]">a</span>
42
+ <span className="text-[#FDCB6E]">y</span>
43
+ <span className="text-[#FF6B6B]">G</span>
44
+ <span className="text-[#4ECDC4]">o</span>
45
+ <span className="ml-2 text-[#45B7D1]">A</span>
46
+ <span className="text-[#FDCB6E]">I</span>
47
+ </Link>
48
+ <nav className="flex items-center justify-center flex-1 space-x-8">
49
+ <Link
50
+ href="/for-teachers"
51
+ className="text-text-secondary hover:text-primary"
52
+ >
53
+ 我是老師
54
+ </Link>
55
+ <Link
56
+ href="/for-students"
57
+ className="text-text-secondary hover:text-primary"
58
+ >
59
+ 我是學生
60
+ </Link>
61
+ </nav>
62
+ <div className="w-9 h-9" />{" "}
63
+ {/* Placeholder maintaining layout consistency during hydration */}
64
+ </div>
65
+ </header>
66
+ );
67
+ }
68
+
69
+ // Post-hydration render - includes full functionality with theme toggle
70
+ return (
71
+ <header className="fixed w-full bg-background-secondary/80 backdrop-blur-sm border-b border-border z-50">
72
+ <div className="container mx-auto px-4 py-4 flex items-center">
73
+ {/* Brand logo with multi-colored letters */}
74
+ <Link href="/" className="text-3xl font-bold">
75
+ <span className="text-[#FF6B6B]">P</span>
76
+ <span className="text-[#4ECDC4]">l</span>
77
+ <span className="text-[#45B7D1]">a</span>
78
+ <span className="text-[#FDCB6E]">y</span>
79
+ <span className="text-[#FF6B6B]">G</span>
80
+ <span className="text-[#4ECDC4]">o</span>
81
+ <span className="ml-2 text-[#45B7D1]">A</span>
82
+ <span className="text-[#FDCB6E]">I</span>
83
+ </Link>
84
+
85
+ {/* Navigation links with language-specific text (Traditional Chinese) */}
86
+ <nav className="flex items-center justify-center flex-1 space-x-8">
87
+ <Link
88
+ href="/for-teachers"
89
+ className="text-text-secondary hover:text-primary"
90
+ >
91
+ 我是老師
92
+ </Link>
93
+ <Link
94
+ href="/for-students"
95
+ className="text-text-secondary hover:text-primary"
96
+ >
97
+ 我是學生
98
+ </Link>
99
+ </nav>
100
+
101
+ {/* Theme switcher dropdown with animated sun/moon icons */}
102
+ <DropdownMenu>
103
+ <DropdownMenuTrigger asChild>
104
+ <Button>
105
+ {/* Animated sun/moon icons that rotate based on theme */}
106
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
107
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
108
+ <span className="sr-only">Toggle theme</span>
109
+ </Button>
110
+ </DropdownMenuTrigger>
111
+ {/* Theme selection menu */}
112
+ <DropdownMenuContent align="end">
113
+ <DropdownMenuItem onClick={() => setTheme("light")}>
114
+ Light
115
+ </DropdownMenuItem>
116
+ <DropdownMenuItem onClick={() => setTheme("dark")}>
117
+ Dark
118
+ </DropdownMenuItem>
119
+ <DropdownMenuItem onClick={() => setTheme("system")}>
120
+ System
121
+ </DropdownMenuItem>
122
+ </DropdownMenuContent>
123
+ </DropdownMenu>
124
+ </div>
125
+ </header>
126
+ );
127
+ }
app/globals.css CHANGED
@@ -5,24 +5,24 @@
5
  @layer base {
6
  :root {
7
  --background: 0 0% 100%;
8
- --foreground: 20 14.3% 4.1%;
9
  --card: 0 0% 100%;
10
- --card-foreground: 20 14.3% 4.1%;
11
  --popover: 0 0% 100%;
12
- --popover-foreground: 20 14.3% 4.1%;
13
- --primary: 24.6 95% 53.1%;
14
- --primary-foreground: 60 9.1% 97.8%;
15
- --secondary: 60 4.8% 95.9%;
16
- --secondary-foreground: 24 9.8% 10%;
17
- --muted: 60 4.8% 95.9%;
18
- --muted-foreground: 25 5.3% 44.7%;
19
- --accent: 60 4.8% 95.9%;
20
- --accent-foreground: 24 9.8% 10%;
21
  --destructive: 0 84.2% 60.2%;
22
- --destructive-foreground: 60 9.1% 97.8%;
23
- --border: 20 5.9% 90%;
24
- --input: 20 5.9% 90%;
25
- --ring: 24.6 95% 53.1%;
26
  --radius: 1rem;
27
  --chart-1: 12 76% 61%;
28
  --chart-2: 173 58% 39%;
@@ -33,24 +33,24 @@
33
 
34
  .dark {
35
  --background: 20 14.3% 4.1%;
36
- --foreground: 60 9.1% 97.8%;
37
- --card: 20 14.3% 4.1%;
38
- --card-foreground: 60 9.1% 97.8%;
39
- --popover: 20 14.3% 4.1%;
40
- --popover-foreground: 60 9.1% 97.8%;
41
- --primary: 20.5 90.2% 48.2%;
42
- --primary-foreground: 60 9.1% 97.8%;
43
- --secondary: 12 6.5% 15.1%;
44
- --secondary-foreground: 60 9.1% 97.8%;
45
- --muted: 12 6.5% 15.1%;
46
- --muted-foreground: 24 5.4% 63.9%;
47
  --accent: 12 6.5% 15.1%;
48
- --accent-foreground: 60 9.1% 97.8%;
49
- --destructive: 0 72.2% 50.6%;
50
- --destructive-foreground: 60 9.1% 97.8%;
51
- --border: 12 6.5% 15.1%;
52
- --input: 12 6.5% 15.1%;
53
- --ring: 20.5 90.2% 48.2%;
54
  --chart-1: 220 70% 50%;
55
  --chart-2: 160 60% 45%;
56
  --chart-3: 30 80% 55%;
 
5
  @layer base {
6
  :root {
7
  --background: 0 0% 100%;
8
+ --foreground: 240 10% 3.9%;
9
  --card: 0 0% 100%;
10
+ --card-foreground: 240 10% 3.9%;
11
  --popover: 0 0% 100%;
12
+ --popover-foreground: 240 10% 3.9%;
13
+ --primary: 351.3 94.5% 71.4%;
14
+ --primary-foreground: 355.7 100% 97.3%;
15
+ --secondary: 240 4.8% 95.9%;
16
+ --secondary-foreground: 240 5.9% 10%;
17
+ --muted: 240 4.8% 95.9%;
18
+ --muted-foreground: 240 3.8% 46.1%;
19
+ --accent: 240 4.8% 95.9%;
20
+ --accent-foreground: 240 5.9% 10%;
21
  --destructive: 0 84.2% 60.2%;
22
+ --destructive-foreground: 0 0% 98%;
23
+ --border: 240 5.9% 90%;
24
+ --input: 240 5.9% 90%;
25
+ --ring: 351.3 94.5% 71.4%;
26
  --radius: 1rem;
27
  --chart-1: 12 76% 61%;
28
  --chart-2: 173 58% 39%;
 
33
 
34
  .dark {
35
  --background: 20 14.3% 4.1%;
36
+ --foreground: 0 0% 95%;
37
+ --card: 24 9.8% 10%;
38
+ --card-foreground: 0 0% 95%;
39
+ --popover: 0 0% 9%;
40
+ --popover-foreground: 0 0% 95%;
41
+ --primary: 351.3 94.5% 71.4%;
42
+ --primary-foreground: 355.7 100% 97.3%;
43
+ --secondary: 240 3.7% 15.9%;
44
+ --secondary-foreground: 0 0% 98%;
45
+ --muted: 0 0% 15%;
46
+ --muted-foreground: 240 5% 64.9%;
47
  --accent: 12 6.5% 15.1%;
48
+ --accent-foreground: 0 0% 98%;
49
+ --destructive: 0 62.8% 30.6%;
50
+ --destructive-foreground: 0 85.7% 97.3%;
51
+ --border: 240 3.7% 15.9%;
52
+ --input: 240 3.7% 15.9%;
53
+ --ring: 351.3 94.5% 71.4%;
54
  --chart-1: 220 70% 50%;
55
  --chart-2: 160 60% 45%;
56
  --chart-3: 30 80% 55%;
app/layout.tsx CHANGED
@@ -1,8 +1,8 @@
1
  import type { Metadata } from "next";
2
  import localFont from "next/font/local";
3
  import "./globals.css";
4
- import { ThemeProvider } from '@/components/custom/theme-provider'
5
- import { TopNav } from '../components/custom/top-nav'
6
 
7
  const geistSans = localFont({
8
  src: "./fonts/GeistVF.woff",
@@ -12,16 +12,21 @@ const geistSans = localFont({
12
 
13
  export const metadata: Metadata = {
14
  title: "PlayGo AI - AI-Powered Learning Platform",
15
- description: "Transform education with AI-powered interactive learning experiences",
 
16
  };
17
 
18
  export default function RootLayout({
19
  children,
20
  }: {
21
- children: React.ReactNode
22
  }) {
23
  return (
24
- <html lang="en" suppressHydrationWarning className={`${geistSans.variable}`}>
 
 
 
 
25
  <body className="antialiased">
26
  <ThemeProvider
27
  attribute="class"
@@ -30,11 +35,9 @@ export default function RootLayout({
30
  disableTransitionOnChange
31
  >
32
  <TopNav />
33
- <main className="pt-14">
34
- {children}
35
- </main>
36
  </ThemeProvider>
37
  </body>
38
  </html>
39
- )
40
  }
 
1
  import type { Metadata } from "next";
2
  import localFont from "next/font/local";
3
  import "./globals.css";
4
+ import { ThemeProvider } from "@/app/components/theme-provider";
5
+ import { TopNav } from "./components/top-nav";
6
 
7
  const geistSans = localFont({
8
  src: "./fonts/GeistVF.woff",
 
12
 
13
  export const metadata: Metadata = {
14
  title: "PlayGo AI - AI-Powered Learning Platform",
15
+ description:
16
+ "Transform education with AI-powered interactive learning experiences",
17
  };
18
 
19
  export default function RootLayout({
20
  children,
21
  }: {
22
+ children: React.ReactNode;
23
  }) {
24
  return (
25
+ <html
26
+ lang="en"
27
+ suppressHydrationWarning
28
+ className={`${geistSans.variable}`}
29
+ >
30
  <body className="antialiased">
31
  <ThemeProvider
32
  attribute="class"
 
35
  disableTransitionOnChange
36
  >
37
  <TopNav />
38
+ <main className="pt-14">{children}</main>
 
 
39
  </ThemeProvider>
40
  </body>
41
  </html>
42
+ );
43
  }
app/page.tsx CHANGED
@@ -1,68 +1,131 @@
1
- 'use client'
2
 
3
- import Link from "next/link"
4
- import ChatBot from '@/components/custom/LandingPageChatBot'
5
 
 
 
 
 
 
6
  export default function LandingPage() {
7
  return (
8
  <div className="min-h-screen bg-background-primary">
9
  <main className="pt-4">
10
- {/* Hero Section */}
11
  <div className="">
12
- <ChatBot />
13
  </div>
14
 
15
- {/* Features Grid */}
16
  <section className="bg-background-secondary py-20">
17
  <div className="container mx-auto px-4">
18
- <h2 className="text-3xl font-bold text-center mb-16">Build with our powerful learning tools</h2>
 
 
19
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
20
  <div className="p-6 rounded-xl bg-background-primary">
21
  <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
22
- <svg className="w-6 h-6 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
23
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
 
 
 
 
 
 
 
 
 
 
24
  </svg>
25
  </div>
26
- <h3 className="text-xl font-semibold mb-2">Interactive Learning</h3>
27
- <p className="text-text-secondary">Engage with AI-powered quizzes, projects, and virtual labs</p>
 
 
 
 
28
  </div>
29
 
30
  <div className="p-6 rounded-xl bg-background-primary">
31
  <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
32
- <svg className="w-6 h-6 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
33
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
 
 
 
 
 
 
 
 
 
 
34
  </svg>
35
  </div>
36
  <h3 className="text-xl font-semibold mb-2">Diverse Subjects</h3>
37
- <p className="text-text-secondary">Explore topics from STEM to humanities with expert AI guidance</p>
 
 
38
  </div>
39
 
40
  <div className="p-6 rounded-xl bg-background-primary">
41
  <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
42
- <svg className="w-6 h-6 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
43
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
 
 
 
 
 
 
 
 
 
 
44
  </svg>
45
  </div>
46
- <h3 className="text-xl font-semibold mb-2">Personalized Path</h3>
47
- <p className="text-text-secondary">Adaptive learning tailored to your unique pace and style</p>
 
 
 
 
48
  </div>
49
 
50
  <div className="p-6 rounded-xl bg-background-primary">
51
  <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
52
- <svg className="w-6 h-6 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
 
 
 
 
 
 
 
 
 
 
54
  </svg>
55
  </div>
56
- <h3 className="text-xl font-semibold mb-2">Progress Tracking</h3>
57
- <p className="text-text-secondary">Monitor learning outcomes with detailed analytics</p>
 
 
 
 
58
  </div>
59
  </div>
60
  </div>
61
  </section>
62
 
63
- {/* Stats Section */}
64
  <section className="container mx-auto px-4 py-20">
65
- <h2 className="text-3xl font-bold text-center mb-16">Trusted by educators worldwide</h2>
 
 
66
  <div className="grid grid-cols-2 md:grid-cols-4 gap-8 text-center">
67
  <div>
68
  <p className="text-4xl font-bold text-primary mb-2">500K+</p>
@@ -84,9 +147,16 @@ export default function LandingPage() {
84
  </section>
85
  </main>
86
 
87
- {/* Updated Educational Footer */}
 
 
 
 
 
 
88
  <footer className="bg-background-secondary border-t border-border py-12">
89
  <div className="container mx-auto px-4">
 
90
  <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
91
  <div className="text-center md:text-left">
92
  <h3 className="text-2xl font-bold mb-4">
@@ -99,48 +169,110 @@ export default function LandingPage() {
99
  <span className="ml-2 text-[#45B7D1]">A</span>
100
  <span className="text-[#FDCB6E]">I</span>
101
  </h3>
102
- <p className="text-text-secondary">Making learning fun and accessible for everyone</p>
 
 
103
  </div>
104
 
 
105
  <div className="text-center">
106
  <h3 className="font-semibold mb-4">Learning Resources</h3>
107
  <ul className="space-y-2">
108
- <li><Link href="/library" className="text-text-secondary hover:text-primary">Learning Library</Link></li>
109
- <li><Link href="/tutorials" className="text-text-secondary hover:text-primary">Tutorials</Link></li>
110
- <li><Link href="/blog" className="text-text-secondary hover:text-primary">Educational Blog</Link></li>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  </ul>
112
  </div>
113
 
 
114
  <div className="text-center md:text-right">
115
  <h3 className="font-semibold mb-4">Connect With Us</h3>
116
  <div className="flex justify-center md:justify-end space-x-4">
117
- <a href="#" className="text-text-secondary hover:text-primary" aria-label="Discord">
118
- <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
119
- <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
 
 
 
 
 
 
 
 
120
  </svg>
121
  </a>
122
- <a href="#" className="text-text-secondary hover:text-primary" aria-label="Twitter">
123
- <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
 
 
 
 
 
 
 
 
124
  <path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
125
  </svg>
126
  </a>
127
- <a href="#" className="text-text-secondary hover:text-primary" aria-label="YouTube">
128
- <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
129
- <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
 
 
 
 
 
 
 
 
130
  </svg>
131
  </a>
132
  </div>
133
  <p className="mt-4 text-text-secondary">
134
- Contact us: <a href="mailto:hello@playgoai.org" className="hover:text-primary">hello@playgoai.org</a>
 
 
 
 
 
 
135
  </p>
136
  </div>
137
  </div>
 
 
138
  <div className="mt-12 pt-8 border-t border-border/50 text-center text-text-secondary">
139
- <p>PlayGo AI - A non-profit organization dedicated to making education accessible through AI</p>
140
- <p className="mt-2">&copy; {new Date().getFullYear()} PlayGo AI. All rights reserved.</p>
 
 
 
 
 
141
  </div>
142
  </div>
143
  </footer>
144
  </div>
145
- )
146
- }
 
1
+ "use client";
2
 
3
+ import Link from "next/link";
4
+ import LandingPageChatBot from "@/app/components/landing-page-chat-bot";
5
 
6
+ /**
7
+ * LandingPage Component
8
+ * Main landing page for PlayGo AI educational platform
9
+ * Features a chatbot, feature highlights, statistics, and footer
10
+ */
11
  export default function LandingPage() {
12
  return (
13
  <div className="min-h-screen bg-background-primary">
14
  <main className="pt-4">
15
+ {/* Interactive ChatBot Section */}
16
  <div className="">
17
+ <LandingPageChatBot />
18
  </div>
19
 
20
+ {/* Features Grid Section - Displays 4 key platform features with icons */}
21
  <section className="bg-background-secondary py-20">
22
  <div className="container mx-auto px-4">
23
+ <h2 className="text-3xl font-bold text-center mb-16">
24
+ Build with our powerful learning tools
25
+ </h2>
26
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
27
  <div className="p-6 rounded-xl bg-background-primary">
28
  <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
29
+ <svg
30
+ className="w-6 h-6 text-primary"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ viewBox="0 0 24 24"
34
+ >
35
+ <path
36
+ strokeLinecap="round"
37
+ strokeLinejoin="round"
38
+ strokeWidth={2}
39
+ d="M13 10V3L4 14h7v7l9-11h-7z"
40
+ />
41
  </svg>
42
  </div>
43
+ <h3 className="text-xl font-semibold mb-2">
44
+ Interactive Learning
45
+ </h3>
46
+ <p className="text-text-secondary">
47
+ Engage with AI-powered quizzes, projects, and virtual labs
48
+ </p>
49
  </div>
50
 
51
  <div className="p-6 rounded-xl bg-background-primary">
52
  <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
53
+ <svg
54
+ className="w-6 h-6 text-primary"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ viewBox="0 0 24 24"
58
+ >
59
+ <path
60
+ strokeLinecap="round"
61
+ strokeLinejoin="round"
62
+ strokeWidth={2}
63
+ d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
64
+ />
65
  </svg>
66
  </div>
67
  <h3 className="text-xl font-semibold mb-2">Diverse Subjects</h3>
68
+ <p className="text-text-secondary">
69
+ Explore topics from STEM to humanities with expert AI guidance
70
+ </p>
71
  </div>
72
 
73
  <div className="p-6 rounded-xl bg-background-primary">
74
  <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
75
+ <svg
76
+ className="w-6 h-6 text-primary"
77
+ fill="none"
78
+ stroke="currentColor"
79
+ viewBox="0 0 24 24"
80
+ >
81
+ <path
82
+ strokeLinecap="round"
83
+ strokeLinejoin="round"
84
+ strokeWidth={2}
85
+ d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
86
+ />
87
  </svg>
88
  </div>
89
+ <h3 className="text-xl font-semibold mb-2">
90
+ Personalized Path
91
+ </h3>
92
+ <p className="text-text-secondary">
93
+ Adaptive learning tailored to your unique pace and style
94
+ </p>
95
  </div>
96
 
97
  <div className="p-6 rounded-xl bg-background-primary">
98
  <div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4">
99
+ <svg
100
+ className="w-6 h-6 text-primary"
101
+ fill="none"
102
+ stroke="currentColor"
103
+ viewBox="0 0 24 24"
104
+ >
105
+ <path
106
+ strokeLinecap="round"
107
+ strokeLinejoin="round"
108
+ strokeWidth={2}
109
+ d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
110
+ />
111
  </svg>
112
  </div>
113
+ <h3 className="text-xl font-semibold mb-2">
114
+ Progress Tracking
115
+ </h3>
116
+ <p className="text-text-secondary">
117
+ Monitor learning outcomes with detailed analytics
118
+ </p>
119
  </div>
120
  </div>
121
  </div>
122
  </section>
123
 
124
+ {/* Statistics Section - Displays key metrics about platform usage */}
125
  <section className="container mx-auto px-4 py-20">
126
+ <h2 className="text-3xl font-bold text-center mb-16">
127
+ Trusted by educators worldwide
128
+ </h2>
129
  <div className="grid grid-cols-2 md:grid-cols-4 gap-8 text-center">
130
  <div>
131
  <p className="text-4xl font-bold text-primary mb-2">500K+</p>
 
147
  </section>
148
  </main>
149
 
150
+ {/* Footer Section
151
+ * Contains:
152
+ * - Branded logo with playful multi-colored letters
153
+ * - Learning resource links
154
+ * - Social media links and contact information
155
+ * - Non-profit mission statement and copyright
156
+ */}
157
  <footer className="bg-background-secondary border-t border-border py-12">
158
  <div className="container mx-auto px-4">
159
+ {/* Logo and Tagline */}
160
  <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
161
  <div className="text-center md:text-left">
162
  <h3 className="text-2xl font-bold mb-4">
 
169
  <span className="ml-2 text-[#45B7D1]">A</span>
170
  <span className="text-[#FDCB6E]">I</span>
171
  </h3>
172
+ <p className="text-text-secondary">
173
+ Making learning fun and accessible for everyone
174
+ </p>
175
  </div>
176
 
177
+ {/* Learning Resources Navigation */}
178
  <div className="text-center">
179
  <h3 className="font-semibold mb-4">Learning Resources</h3>
180
  <ul className="space-y-2">
181
+ <li>
182
+ <Link
183
+ href="/library"
184
+ className="text-text-secondary hover:text-primary"
185
+ >
186
+ Learning Library
187
+ </Link>
188
+ </li>
189
+ <li>
190
+ <Link
191
+ href="/tutorials"
192
+ className="text-text-secondary hover:text-primary"
193
+ >
194
+ Tutorials
195
+ </Link>
196
+ </li>
197
+ <li>
198
+ <Link
199
+ href="/blog"
200
+ className="text-text-secondary hover:text-primary"
201
+ >
202
+ Educational Blog
203
+ </Link>
204
+ </li>
205
  </ul>
206
  </div>
207
 
208
+ {/* Social Media and Contact Information */}
209
  <div className="text-center md:text-right">
210
  <h3 className="font-semibold mb-4">Connect With Us</h3>
211
  <div className="flex justify-center md:justify-end space-x-4">
212
+ <a
213
+ href="#"
214
+ className="text-text-secondary hover:text-primary"
215
+ aria-label="Discord"
216
+ >
217
+ <svg
218
+ className="w-6 h-6"
219
+ fill="currentColor"
220
+ viewBox="0 0 24 24"
221
+ >
222
+ <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
223
  </svg>
224
  </a>
225
+ <a
226
+ href="#"
227
+ className="text-text-secondary hover:text-primary"
228
+ aria-label="Twitter"
229
+ >
230
+ <svg
231
+ className="w-6 h-6"
232
+ fill="currentColor"
233
+ viewBox="0 0 24 24"
234
+ >
235
  <path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
236
  </svg>
237
  </a>
238
+ <a
239
+ href="#"
240
+ className="text-text-secondary hover:text-primary"
241
+ aria-label="YouTube"
242
+ >
243
+ <svg
244
+ className="w-6 h-6"
245
+ fill="currentColor"
246
+ viewBox="0 0 24 24"
247
+ >
248
+ <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
249
  </svg>
250
  </a>
251
  </div>
252
  <p className="mt-4 text-text-secondary">
253
+ Contact us:{" "}
254
+ <a
255
+ href="mailto:hello@playgoai.org"
256
+ className="hover:text-primary"
257
+ >
258
+ hello@playgoai.org
259
+ </a>
260
  </p>
261
  </div>
262
  </div>
263
+
264
+ {/* Non-profit Mission and Copyright */}
265
  <div className="mt-12 pt-8 border-t border-border/50 text-center text-text-secondary">
266
+ <p>
267
+ PlayGo AI - A non-profit organization dedicated to making
268
+ education accessible through AI
269
+ </p>
270
+ <p className="mt-2">
271
+ &copy; {new Date().getFullYear()} PlayGo AI. All rights reserved.
272
+ </p>
273
  </div>
274
  </div>
275
  </footer>
276
  </div>
277
+ );
278
+ }
app/types/chatbot.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ChatBot {
2
+ id: string;
3
+ title: string;
4
+ description: string;
5
+ subject: string;
6
+ icon: string;
7
+ category: string;
8
+ popular?: boolean;
9
+ trending?: boolean;
10
+ }
components.json CHANGED
@@ -18,4 +18,4 @@
18
  "hooks": "@/hooks"
19
  },
20
  "iconLibrary": "lucide"
21
- }
 
18
  "hooks": "@/hooks"
19
  },
20
  "iconLibrary": "lucide"
21
+ }
components/custom/ChatBotCard.tsx DELETED
@@ -1,43 +0,0 @@
1
- import { Card, CardHeader, CardContent, CardFooter } from "@/components/ui/card"
2
-
3
- interface ChatBotCardProps {
4
- chatbot: {
5
- id: string
6
- title: string
7
- description: string
8
- subject: string
9
- icon: string
10
- author?: string
11
- }
12
- }
13
-
14
- export default function ChatBotCard({ chatbot }: ChatBotCardProps) {
15
- return (
16
- <Card className="group relative overflow-hidden bg-background-primary/50 shadow-sm transition-all hover:shadow-md backdrop-blur-sm">
17
- <CardHeader className="space-y-0">
18
- <div className="flex items-center justify-between">
19
- <div className="flex items-center gap-3">
20
- <span className="text-3xl">{chatbot.icon}</span>
21
- <h3 className="text-xl font-semibold text-text-primary">{chatbot.title}</h3>
22
- </div>
23
- <span className="rounded-full bg-background-secondary/50 px-3 py-1 text-sm text-text-secondary">
24
- {chatbot.subject}
25
- </span>
26
- </div>
27
- </CardHeader>
28
-
29
- <CardContent>
30
- <p className="text-text-secondary">{chatbot.description}</p>
31
- {chatbot.author && (
32
- <p className="mt-2 text-sm text-text-secondary">作者:{chatbot.author}</p>
33
- )}
34
- </CardContent>
35
-
36
- <CardFooter>
37
- <button className="w-full rounded-xl bg-primary px-4 py-2 text-white transition-colors hover:bg-primary/90">
38
- 開始對話
39
- </button>
40
- </CardFooter>
41
- </Card>
42
- )
43
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/custom/ChatBotGrid.tsx DELETED
@@ -1,60 +0,0 @@
1
- 'use client'
2
-
3
- import { useState } from 'react'
4
- import ChatBotCard from './ChatBotCard'
5
-
6
- interface ChatBot {
7
- id: string
8
- title: string
9
- description: string
10
- subject: string
11
- icon: string
12
- author?: string
13
- }
14
-
15
- const SAMPLE_CHATBOTS: ChatBot[] = [
16
- {
17
- id: '1',
18
- title: 'Code Tutor',
19
- description: "Let's code together! I'm Khanmigo Lite, by Khan Academy.",
20
- subject: '程式設計',
21
- icon: '👨‍💻',
22
- author: 'khanacademy.org'
23
- },
24
- {
25
- id: '2',
26
- title: 'Whimsical Diagrams',
27
- description: 'Explains and visualizes concepts with flowcharts, mindmaps and sequence diagrams.',
28
- subject: '視覺化',
29
- icon: '📊',
30
- author: 'whimsical.com'
31
- },
32
- {
33
- id: '3',
34
- title: 'Resume',
35
- description: 'By combining the expertise of top resume writers with advanced AI, we assist in diagnosing and improving resumes.',
36
- subject: '求職',
37
- icon: '📝',
38
- author: 'jobright.ai'
39
- },
40
- {
41
- id: '4',
42
- title: 'Universal Primer',
43
- description: 'The fastest way to learn anything hard.',
44
- subject: '學習',
45
- icon: '📚',
46
- author: 'Siqi Chen'
47
- }
48
- ]
49
-
50
- export default function ChatBotGrid() {
51
- const [chatbots] = useState<ChatBot[]>(SAMPLE_CHATBOTS)
52
-
53
- return (
54
- <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-2">
55
- {chatbots.map((chatbot) => (
56
- <ChatBotCard key={chatbot.id} chatbot={chatbot} />
57
- ))}
58
- </div>
59
- )
60
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/custom/SubjectFilter.tsx DELETED
@@ -1,19 +0,0 @@
1
- 'use client'
2
-
3
- export default function SubjectFilter() {
4
- const subjects = ['熱門精選', '寫作', '生產力', '研究與分析', '教育', '日常生活', '程式設計']
5
-
6
- return (
7
- <div className="flex flex-wrap gap-4">
8
- {subjects.map((subject) => (
9
- <button
10
- key={subject}
11
- className="rounded-full border border-border/50 px-6 py-2 text-base transition-colors
12
- hover:bg-background-secondary/50 focus:outline-none focus:ring-2 focus:ring-primary/50"
13
- >
14
- {subject}
15
- </button>
16
- ))}
17
- </div>
18
- )
19
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/custom/theme-provider.tsx DELETED
@@ -1,22 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { ThemeProvider as NextThemesProvider } from "next-themes"
5
-
6
- export function ThemeProvider({
7
- children,
8
- ...props
9
- }: React.ComponentProps<typeof NextThemesProvider>) {
10
- const [mounted, setMounted] = React.useState(false)
11
-
12
- // Prevent hydration mismatch by only rendering after mount
13
- React.useEffect(() => {
14
- setMounted(true)
15
- }, [])
16
-
17
- if (!mounted) {
18
- return null
19
- }
20
-
21
- return <NextThemesProvider {...props}>{children}</NextThemesProvider>
22
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/custom/top-nav.tsx DELETED
@@ -1,96 +0,0 @@
1
- 'use client'
2
-
3
- import * as React from 'react'
4
- import { Moon, Sun } from "lucide-react"
5
- import { useTheme } from "next-themes"
6
- import Link from 'next/link'
7
- import { Button } from "@/components/ui/button"
8
- import {
9
- DropdownMenu,
10
- DropdownMenuContent,
11
- DropdownMenuItem,
12
- DropdownMenuTrigger,
13
- } from "@/components/ui/dropdown-menu"
14
-
15
- export function TopNav() {
16
- const { setTheme } = useTheme()
17
- const [mounted, setMounted] = React.useState(false)
18
-
19
- React.useEffect(() => {
20
- setMounted(true)
21
- }, [])
22
-
23
- // Prevent theme toggle from showing before client-side hydration
24
- if (!mounted) {
25
- return (
26
- <header className="fixed w-full bg-background-secondary/80 backdrop-blur-sm border-b border-border z-50">
27
- <div className="container mx-auto px-4 py-4 flex items-center">
28
- <Link href="/" className="text-3xl font-bold">
29
- <span className="text-[#FF6B6B]">P</span>
30
- <span className="text-[#4ECDC4]">l</span>
31
- <span className="text-[#45B7D1]">a</span>
32
- <span className="text-[#FDCB6E]">y</span>
33
- <span className="text-[#FF6B6B]">G</span>
34
- <span className="text-[#4ECDC4]">o</span>
35
- <span className="ml-2 text-[#45B7D1]">A</span>
36
- <span className="text-[#FDCB6E]">I</span>
37
- </Link>
38
- <nav className="flex items-center justify-center flex-1 space-x-8">
39
- <Link href="/for-teachers" className="text-text-secondary hover:text-primary">
40
- 我是老師
41
- </Link>
42
- <Link href="/for-students" className="text-text-secondary hover:text-primary">
43
- 我是學生
44
- </Link>
45
- </nav>
46
- <div className="w-9 h-9" /> {/* Placeholder for theme toggle */}
47
- </div>
48
- </header>
49
- )
50
- }
51
-
52
- return (
53
- <header className="fixed w-full bg-background-secondary/80 backdrop-blur-sm border-b border-border z-50">
54
- <div className="container mx-auto px-4 py-4 flex items-center">
55
- <Link href="/" className="text-3xl font-bold">
56
- <span className="text-[#FF6B6B]">P</span>
57
- <span className="text-[#4ECDC4]">l</span>
58
- <span className="text-[#45B7D1]">a</span>
59
- <span className="text-[#FDCB6E]">y</span>
60
- <span className="text-[#FF6B6B]">G</span>
61
- <span className="text-[#4ECDC4]">o</span>
62
- <span className="ml-2 text-[#45B7D1]">A</span>
63
- <span className="text-[#FDCB6E]">I</span>
64
- </Link>
65
- <nav className="flex items-center justify-center flex-1 space-x-8">
66
- <Link href="/for-teachers" className="text-text-secondary hover:text-primary">
67
- 我是老師
68
- </Link>
69
- <Link href="/for-students" className="text-text-secondary hover:text-primary">
70
- 我是學生
71
- </Link>
72
- </nav>
73
- <DropdownMenu>
74
- <DropdownMenuTrigger asChild>
75
- <Button>
76
- <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
77
- <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
78
- <span className="sr-only">Toggle theme</span>
79
- </Button>
80
- </DropdownMenuTrigger>
81
- <DropdownMenuContent align="end">
82
- <DropdownMenuItem onClick={() => setTheme("light")}>
83
- Light
84
- </DropdownMenuItem>
85
- <DropdownMenuItem onClick={() => setTheme("dark")}>
86
- Dark
87
- </DropdownMenuItem>
88
- <DropdownMenuItem onClick={() => setTheme("system")}>
89
- System
90
- </DropdownMenuItem>
91
- </DropdownMenuContent>
92
- </DropdownMenu>
93
- </div>
94
- </header>
95
- )
96
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/badge.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13
+ secondary:
14
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15
+ destructive:
16
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17
+ outline: "text-foreground",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ },
23
+ },
24
+ );
25
+
26
+ export interface BadgeProps
27
+ extends React.HTMLAttributes<HTMLDivElement>,
28
+ VariantProps<typeof badgeVariants> {}
29
+
30
+ function Badge({ className, variant, ...props }: BadgeProps) {
31
+ return (
32
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
+ );
34
+ }
35
+
36
+ export { Badge, badgeVariants };
components/ui/button.tsx CHANGED
@@ -1,11 +1,11 @@
1
- import * as React from "react"
2
- import { Slot } from "@radix-ui/react-slot"
3
- import { cva, type VariantProps } from "class-variance-authority"
4
 
5
- import { cn } from "@/lib/utils"
6
 
7
  const buttonVariants = cva(
8
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
  {
10
  variants: {
11
  variant: {
@@ -31,27 +31,27 @@ const buttonVariants = cva(
31
  variant: "default",
32
  size: "default",
33
  },
34
- }
35
- )
36
 
37
  export interface ButtonProps
38
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
  VariantProps<typeof buttonVariants> {
40
- asChild?: boolean
41
  }
42
 
43
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
  ({ className, variant, size, asChild = false, ...props }, ref) => {
45
- const Comp = asChild ? Slot : "button"
46
  return (
47
  <Comp
48
  className={cn(buttonVariants({ variant, size, className }))}
49
  ref={ref}
50
  {...props}
51
  />
52
- )
53
- }
54
- )
55
- Button.displayName = "Button"
56
 
57
- export { Button, buttonVariants }
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
 
5
+ import { cn } from "@/lib/utils";
6
 
7
  const buttonVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9
  {
10
  variants: {
11
  variant: {
 
31
  variant: "default",
32
  size: "default",
33
  },
34
+ },
35
+ );
36
 
37
  export interface ButtonProps
38
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
  VariantProps<typeof buttonVariants> {
40
+ asChild?: boolean;
41
  }
42
 
43
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
  ({ className, variant, size, asChild = false, ...props }, ref) => {
45
+ const Comp = asChild ? Slot : "button";
46
  return (
47
  <Comp
48
  className={cn(buttonVariants({ variant, size, className }))}
49
  ref={ref}
50
  {...props}
51
  />
52
+ );
53
+ },
54
+ );
55
+ Button.displayName = "Button";
56
 
57
+ export { Button, buttonVariants };
components/ui/card.tsx CHANGED
@@ -1,6 +1,6 @@
1
- import * as React from "react"
2
 
3
- import { cn } from "@/lib/utils"
4
 
5
  const Card = React.forwardRef<
6
  HTMLDivElement,
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
10
  ref={ref}
11
  className={cn(
12
  "rounded-xl border bg-card text-card-foreground shadow",
13
- className
14
  )}
15
  {...props}
16
  />
17
- ))
18
- Card.displayName = "Card"
19
 
20
  const CardHeader = React.forwardRef<
21
  HTMLDivElement,
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
26
  className={cn("flex flex-col space-y-1.5 p-6", className)}
27
  {...props}
28
  />
29
- ))
30
- CardHeader.displayName = "CardHeader"
31
 
32
  const CardTitle = React.forwardRef<
33
  HTMLDivElement,
@@ -38,8 +38,8 @@ const CardTitle = React.forwardRef<
38
  className={cn("font-semibold leading-none tracking-tight", className)}
39
  {...props}
40
  />
41
- ))
42
- CardTitle.displayName = "CardTitle"
43
 
44
  const CardDescription = React.forwardRef<
45
  HTMLDivElement,
@@ -50,16 +50,16 @@ const CardDescription = React.forwardRef<
50
  className={cn("text-sm text-muted-foreground", className)}
51
  {...props}
52
  />
53
- ))
54
- CardDescription.displayName = "CardDescription"
55
 
56
  const CardContent = React.forwardRef<
57
  HTMLDivElement,
58
  React.HTMLAttributes<HTMLDivElement>
59
  >(({ className, ...props }, ref) => (
60
  <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
61
- ))
62
- CardContent.displayName = "CardContent"
63
 
64
  const CardFooter = React.forwardRef<
65
  HTMLDivElement,
@@ -70,7 +70,14 @@ const CardFooter = React.forwardRef<
70
  className={cn("flex items-center p-6 pt-0", className)}
71
  {...props}
72
  />
73
- ))
74
- CardFooter.displayName = "CardFooter"
75
 
76
- export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
 
3
+ import { cn } from "@/lib/utils";
4
 
5
  const Card = React.forwardRef<
6
  HTMLDivElement,
 
10
  ref={ref}
11
  className={cn(
12
  "rounded-xl border bg-card text-card-foreground shadow",
13
+ className,
14
  )}
15
  {...props}
16
  />
17
+ ));
18
+ Card.displayName = "Card";
19
 
20
  const CardHeader = React.forwardRef<
21
  HTMLDivElement,
 
26
  className={cn("flex flex-col space-y-1.5 p-6", className)}
27
  {...props}
28
  />
29
+ ));
30
+ CardHeader.displayName = "CardHeader";
31
 
32
  const CardTitle = React.forwardRef<
33
  HTMLDivElement,
 
38
  className={cn("font-semibold leading-none tracking-tight", className)}
39
  {...props}
40
  />
41
+ ));
42
+ CardTitle.displayName = "CardTitle";
43
 
44
  const CardDescription = React.forwardRef<
45
  HTMLDivElement,
 
50
  className={cn("text-sm text-muted-foreground", className)}
51
  {...props}
52
  />
53
+ ));
54
+ CardDescription.displayName = "CardDescription";
55
 
56
  const CardContent = React.forwardRef<
57
  HTMLDivElement,
58
  React.HTMLAttributes<HTMLDivElement>
59
  >(({ className, ...props }, ref) => (
60
  <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
61
+ ));
62
+ CardContent.displayName = "CardContent";
63
 
64
  const CardFooter = React.forwardRef<
65
  HTMLDivElement,
 
70
  className={cn("flex items-center p-6 pt-0", className)}
71
  {...props}
72
  />
73
+ ));
74
+ CardFooter.displayName = "CardFooter";
75
 
76
+ export {
77
+ Card,
78
+ CardHeader,
79
+ CardFooter,
80
+ CardTitle,
81
+ CardDescription,
82
+ CardContent,
83
+ };
components/ui/command.tsx ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { type DialogProps } from "@radix-ui/react-dialog";
5
+ import { Command as CommandPrimitive } from "cmdk";
6
+ import { Search } from "lucide-react";
7
+
8
+ import { cn } from "@/lib/utils";
9
+ import { Dialog, DialogContent } from "@/components/ui/dialog";
10
+
11
+ const Command = React.forwardRef<
12
+ React.ElementRef<typeof CommandPrimitive>,
13
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
14
+ >(({ className, ...props }, ref) => (
15
+ <CommandPrimitive
16
+ ref={ref}
17
+ className={cn(
18
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ ));
24
+ Command.displayName = CommandPrimitive.displayName;
25
+
26
+ const CommandDialog = ({ children, ...props }: DialogProps) => {
27
+ return (
28
+ <Dialog {...props}>
29
+ <DialogContent className="overflow-hidden p-0">
30
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
31
+ {children}
32
+ </Command>
33
+ </DialogContent>
34
+ </Dialog>
35
+ );
36
+ };
37
+
38
+ const CommandInput = React.forwardRef<
39
+ React.ElementRef<typeof CommandPrimitive.Input>,
40
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
41
+ >(({ className, ...props }, ref) => (
42
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
43
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
44
+ <CommandPrimitive.Input
45
+ ref={ref}
46
+ className={cn(
47
+ "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ </div>
53
+ ));
54
+
55
+ CommandInput.displayName = CommandPrimitive.Input.displayName;
56
+
57
+ const CommandList = React.forwardRef<
58
+ React.ElementRef<typeof CommandPrimitive.List>,
59
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
60
+ >(({ className, ...props }, ref) => (
61
+ <CommandPrimitive.List
62
+ ref={ref}
63
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
64
+ {...props}
65
+ />
66
+ ));
67
+
68
+ CommandList.displayName = CommandPrimitive.List.displayName;
69
+
70
+ const CommandEmpty = React.forwardRef<
71
+ React.ElementRef<typeof CommandPrimitive.Empty>,
72
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
73
+ >((props, ref) => (
74
+ <CommandPrimitive.Empty
75
+ ref={ref}
76
+ className="py-6 text-center text-sm"
77
+ {...props}
78
+ />
79
+ ));
80
+
81
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
82
+
83
+ const CommandGroup = React.forwardRef<
84
+ React.ElementRef<typeof CommandPrimitive.Group>,
85
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
86
+ >(({ className, ...props }, ref) => (
87
+ <CommandPrimitive.Group
88
+ ref={ref}
89
+ className={cn(
90
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
91
+ className,
92
+ )}
93
+ {...props}
94
+ />
95
+ ));
96
+
97
+ CommandGroup.displayName = CommandPrimitive.Group.displayName;
98
+
99
+ const CommandSeparator = React.forwardRef<
100
+ React.ElementRef<typeof CommandPrimitive.Separator>,
101
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
102
+ >(({ className, ...props }, ref) => (
103
+ <CommandPrimitive.Separator
104
+ ref={ref}
105
+ className={cn("-mx-1 h-px bg-border", className)}
106
+ {...props}
107
+ />
108
+ ));
109
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
110
+
111
+ const CommandItem = React.forwardRef<
112
+ React.ElementRef<typeof CommandPrimitive.Item>,
113
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
114
+ >(({ className, ...props }, ref) => (
115
+ <CommandPrimitive.Item
116
+ ref={ref}
117
+ className={cn(
118
+ "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
119
+ className,
120
+ )}
121
+ {...props}
122
+ />
123
+ ));
124
+
125
+ CommandItem.displayName = CommandPrimitive.Item.displayName;
126
+
127
+ const CommandShortcut = ({
128
+ className,
129
+ ...props
130
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
131
+ return (
132
+ <span
133
+ className={cn(
134
+ "ml-auto text-xs tracking-widest text-muted-foreground",
135
+ className,
136
+ )}
137
+ {...props}
138
+ />
139
+ );
140
+ };
141
+ CommandShortcut.displayName = "CommandShortcut";
142
+
143
+ export {
144
+ Command,
145
+ CommandDialog,
146
+ CommandInput,
147
+ CommandList,
148
+ CommandEmpty,
149
+ CommandGroup,
150
+ CommandItem,
151
+ CommandShortcut,
152
+ CommandSeparator,
153
+ };
components/ui/dialog.tsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { X } from "lucide-react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ const Dialog = DialogPrimitive.Root;
10
+
11
+ const DialogTrigger = DialogPrimitive.Trigger;
12
+
13
+ const DialogPortal = DialogPrimitive.Portal;
14
+
15
+ const DialogClose = DialogPrimitive.Close;
16
+
17
+ const DialogOverlay = React.forwardRef<
18
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
19
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20
+ >(({ className, ...props }, ref) => (
21
+ <DialogPrimitive.Overlay
22
+ ref={ref}
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className,
26
+ )}
27
+ {...props}
28
+ />
29
+ ));
30
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31
+
32
+ const DialogContent = React.forwardRef<
33
+ React.ElementRef<typeof DialogPrimitive.Content>,
34
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
+ >(({ className, children, ...props }, ref) => (
36
+ <DialogPortal>
37
+ <DialogOverlay />
38
+ <DialogPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
42
+ className,
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
48
+ <X className="h-4 w-4" />
49
+ <span className="sr-only">Close</span>
50
+ </DialogPrimitive.Close>
51
+ </DialogPrimitive.Content>
52
+ </DialogPortal>
53
+ ));
54
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
55
+
56
+ const DialogHeader = ({
57
+ className,
58
+ ...props
59
+ }: React.HTMLAttributes<HTMLDivElement>) => (
60
+ <div
61
+ className={cn(
62
+ "flex flex-col space-y-1.5 text-center sm:text-left",
63
+ className,
64
+ )}
65
+ {...props}
66
+ />
67
+ );
68
+ DialogHeader.displayName = "DialogHeader";
69
+
70
+ const DialogFooter = ({
71
+ className,
72
+ ...props
73
+ }: React.HTMLAttributes<HTMLDivElement>) => (
74
+ <div
75
+ className={cn(
76
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
77
+ className,
78
+ )}
79
+ {...props}
80
+ />
81
+ );
82
+ DialogFooter.displayName = "DialogFooter";
83
+
84
+ const DialogTitle = React.forwardRef<
85
+ React.ElementRef<typeof DialogPrimitive.Title>,
86
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
87
+ >(({ className, ...props }, ref) => (
88
+ <DialogPrimitive.Title
89
+ ref={ref}
90
+ className={cn(
91
+ "text-lg font-semibold leading-none tracking-tight",
92
+ className,
93
+ )}
94
+ {...props}
95
+ />
96
+ ));
97
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
98
+
99
+ const DialogDescription = React.forwardRef<
100
+ React.ElementRef<typeof DialogPrimitive.Description>,
101
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
102
+ >(({ className, ...props }, ref) => (
103
+ <DialogPrimitive.Description
104
+ ref={ref}
105
+ className={cn("text-sm text-muted-foreground", className)}
106
+ {...props}
107
+ />
108
+ ));
109
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
110
+
111
+ export {
112
+ Dialog,
113
+ DialogPortal,
114
+ DialogOverlay,
115
+ DialogTrigger,
116
+ DialogClose,
117
+ DialogContent,
118
+ DialogHeader,
119
+ DialogFooter,
120
+ DialogTitle,
121
+ DialogDescription,
122
+ };
components/ui/dropdown-menu.tsx CHANGED
@@ -1,27 +1,27 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
- import { Check, ChevronRight, Circle } from "lucide-react"
6
 
7
- import { cn } from "@/lib/utils"
8
 
9
- const DropdownMenu = DropdownMenuPrimitive.Root
10
 
11
- const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12
 
13
- const DropdownMenuGroup = DropdownMenuPrimitive.Group
14
 
15
- const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16
 
17
- const DropdownMenuSub = DropdownMenuPrimitive.Sub
18
 
19
- const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20
 
21
  const DropdownMenuSubTrigger = React.forwardRef<
22
  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24
- inset?: boolean
25
  }
26
  >(({ className, inset, children, ...props }, ref) => (
27
  <DropdownMenuPrimitive.SubTrigger
@@ -29,16 +29,16 @@ const DropdownMenuSubTrigger = React.forwardRef<
29
  className={cn(
30
  "flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
31
  inset && "pl-8",
32
- className
33
  )}
34
  {...props}
35
  >
36
  {children}
37
  <ChevronRight className="ml-auto" />
38
  </DropdownMenuPrimitive.SubTrigger>
39
- ))
40
  DropdownMenuSubTrigger.displayName =
41
- DropdownMenuPrimitive.SubTrigger.displayName
42
 
43
  const DropdownMenuSubContent = React.forwardRef<
44
  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -48,13 +48,13 @@ const DropdownMenuSubContent = React.forwardRef<
48
  ref={ref}
49
  className={cn(
50
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
51
- className
52
  )}
53
  {...props}
54
  />
55
- ))
56
  DropdownMenuSubContent.displayName =
57
- DropdownMenuPrimitive.SubContent.displayName
58
 
59
  const DropdownMenuContent = React.forwardRef<
60
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -67,18 +67,18 @@ const DropdownMenuContent = React.forwardRef<
67
  className={cn(
68
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
69
  "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",
70
- className
71
  )}
72
  {...props}
73
  />
74
  </DropdownMenuPrimitive.Portal>
75
- ))
76
- DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
77
 
78
  const DropdownMenuItem = React.forwardRef<
79
  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
80
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
81
- inset?: boolean
82
  }
83
  >(({ className, inset, ...props }, ref) => (
84
  <DropdownMenuPrimitive.Item
@@ -86,12 +86,12 @@ const DropdownMenuItem = React.forwardRef<
86
  className={cn(
87
  "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
88
  inset && "pl-8",
89
- className
90
  )}
91
  {...props}
92
  />
93
- ))
94
- DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
95
 
96
  const DropdownMenuCheckboxItem = React.forwardRef<
97
  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -101,7 +101,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
101
  ref={ref}
102
  className={cn(
103
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
104
- className
105
  )}
106
  checked={checked}
107
  {...props}
@@ -113,9 +113,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
113
  </span>
114
  {children}
115
  </DropdownMenuPrimitive.CheckboxItem>
116
- ))
117
  DropdownMenuCheckboxItem.displayName =
118
- DropdownMenuPrimitive.CheckboxItem.displayName
119
 
120
  const DropdownMenuRadioItem = React.forwardRef<
121
  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -125,7 +125,7 @@ const DropdownMenuRadioItem = React.forwardRef<
125
  ref={ref}
126
  className={cn(
127
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
128
- className
129
  )}
130
  {...props}
131
  >
@@ -136,13 +136,13 @@ const DropdownMenuRadioItem = React.forwardRef<
136
  </span>
137
  {children}
138
  </DropdownMenuPrimitive.RadioItem>
139
- ))
140
- DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
141
 
142
  const DropdownMenuLabel = React.forwardRef<
143
  React.ElementRef<typeof DropdownMenuPrimitive.Label>,
144
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
145
- inset?: boolean
146
  }
147
  >(({ className, inset, ...props }, ref) => (
148
  <DropdownMenuPrimitive.Label
@@ -150,12 +150,12 @@ const DropdownMenuLabel = React.forwardRef<
150
  className={cn(
151
  "px-2 py-1.5 text-sm font-semibold",
152
  inset && "pl-8",
153
- className
154
  )}
155
  {...props}
156
  />
157
- ))
158
- DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
159
 
160
  const DropdownMenuSeparator = React.forwardRef<
161
  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@@ -166,8 +166,8 @@ const DropdownMenuSeparator = React.forwardRef<
166
  className={cn("-mx-1 my-1 h-px bg-muted", className)}
167
  {...props}
168
  />
169
- ))
170
- DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
171
 
172
  const DropdownMenuShortcut = ({
173
  className,
@@ -178,9 +178,9 @@ const DropdownMenuShortcut = ({
178
  className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
179
  {...props}
180
  />
181
- )
182
- }
183
- DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
184
 
185
  export {
186
  DropdownMenu,
@@ -198,4 +198,4 @@ export {
198
  DropdownMenuSubContent,
199
  DropdownMenuSubTrigger,
200
  DropdownMenuRadioGroup,
201
- }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5
+ import { Check, ChevronRight, Circle } from "lucide-react";
6
 
7
+ import { cn } from "@/lib/utils";
8
 
9
+ const DropdownMenu = DropdownMenuPrimitive.Root;
10
 
11
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12
 
13
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14
 
15
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16
 
17
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18
 
19
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20
 
21
  const DropdownMenuSubTrigger = React.forwardRef<
22
  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24
+ inset?: boolean;
25
  }
26
  >(({ className, inset, children, ...props }, ref) => (
27
  <DropdownMenuPrimitive.SubTrigger
 
29
  className={cn(
30
  "flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
31
  inset && "pl-8",
32
+ className,
33
  )}
34
  {...props}
35
  >
36
  {children}
37
  <ChevronRight className="ml-auto" />
38
  </DropdownMenuPrimitive.SubTrigger>
39
+ ));
40
  DropdownMenuSubTrigger.displayName =
41
+ DropdownMenuPrimitive.SubTrigger.displayName;
42
 
43
  const DropdownMenuSubContent = React.forwardRef<
44
  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
 
48
  ref={ref}
49
  className={cn(
50
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
51
+ className,
52
  )}
53
  {...props}
54
  />
55
+ ));
56
  DropdownMenuSubContent.displayName =
57
+ DropdownMenuPrimitive.SubContent.displayName;
58
 
59
  const DropdownMenuContent = React.forwardRef<
60
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
 
67
  className={cn(
68
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
69
  "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",
70
+ className,
71
  )}
72
  {...props}
73
  />
74
  </DropdownMenuPrimitive.Portal>
75
+ ));
76
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
77
 
78
  const DropdownMenuItem = React.forwardRef<
79
  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
80
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
81
+ inset?: boolean;
82
  }
83
  >(({ className, inset, ...props }, ref) => (
84
  <DropdownMenuPrimitive.Item
 
86
  className={cn(
87
  "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
88
  inset && "pl-8",
89
+ className,
90
  )}
91
  {...props}
92
  />
93
+ ));
94
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
95
 
96
  const DropdownMenuCheckboxItem = React.forwardRef<
97
  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
 
101
  ref={ref}
102
  className={cn(
103
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
104
+ className,
105
  )}
106
  checked={checked}
107
  {...props}
 
113
  </span>
114
  {children}
115
  </DropdownMenuPrimitive.CheckboxItem>
116
+ ));
117
  DropdownMenuCheckboxItem.displayName =
118
+ DropdownMenuPrimitive.CheckboxItem.displayName;
119
 
120
  const DropdownMenuRadioItem = React.forwardRef<
121
  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
 
125
  ref={ref}
126
  className={cn(
127
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
128
+ className,
129
  )}
130
  {...props}
131
  >
 
136
  </span>
137
  {children}
138
  </DropdownMenuPrimitive.RadioItem>
139
+ ));
140
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
141
 
142
  const DropdownMenuLabel = React.forwardRef<
143
  React.ElementRef<typeof DropdownMenuPrimitive.Label>,
144
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
145
+ inset?: boolean;
146
  }
147
  >(({ className, inset, ...props }, ref) => (
148
  <DropdownMenuPrimitive.Label
 
150
  className={cn(
151
  "px-2 py-1.5 text-sm font-semibold",
152
  inset && "pl-8",
153
+ className,
154
  )}
155
  {...props}
156
  />
157
+ ));
158
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
159
 
160
  const DropdownMenuSeparator = React.forwardRef<
161
  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
 
166
  className={cn("-mx-1 my-1 h-px bg-muted", className)}
167
  {...props}
168
  />
169
+ ));
170
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
171
 
172
  const DropdownMenuShortcut = ({
173
  className,
 
178
  className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
179
  {...props}
180
  />
181
+ );
182
+ };
183
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
184
 
185
  export {
186
  DropdownMenu,
 
198
  DropdownMenuSubContent,
199
  DropdownMenuSubTrigger,
200
  DropdownMenuRadioGroup,
201
+ };
components/ui/input.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6
+ ({ className, type, ...props }, ref) => {
7
+ return (
8
+ <input
9
+ type={type}
10
+ className={cn(
11
+ "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ className,
13
+ )}
14
+ ref={ref}
15
+ {...props}
16
+ />
17
+ );
18
+ },
19
+ );
20
+ Input.displayName = "Input";
21
+
22
+ export { Input };
components/ui/popover.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const Popover = PopoverPrimitive.Root;
9
+
10
+ const PopoverTrigger = PopoverPrimitive.Trigger;
11
+
12
+ const PopoverAnchor = PopoverPrimitive.Anchor;
13
+
14
+ const PopoverContent = React.forwardRef<
15
+ React.ElementRef<typeof PopoverPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
17
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18
+ <PopoverPrimitive.Portal>
19
+ <PopoverPrimitive.Content
20
+ ref={ref}
21
+ align={align}
22
+ sideOffset={sideOffset}
23
+ className={cn(
24
+ "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
25
+ className,
26
+ )}
27
+ {...props}
28
+ />
29
+ </PopoverPrimitive.Portal>
30
+ ));
31
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
32
+
33
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
components/ui/separator.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SeparatorPrimitive from "@radix-ui/react-separator";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const Separator = React.forwardRef<
9
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
11
+ >(
12
+ (
13
+ { className, orientation = "horizontal", decorative = true, ...props },
14
+ ref,
15
+ ) => (
16
+ <SeparatorPrimitive.Root
17
+ ref={ref}
18
+ decorative={decorative}
19
+ orientation={orientation}
20
+ className={cn(
21
+ "shrink-0 bg-border",
22
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ ),
28
+ );
29
+ Separator.displayName = SeparatorPrimitive.Root.displayName;
30
+
31
+ export { Separator };
lib/utils.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { clsx, type ClassValue } from "clsx"
2
- import { twMerge } from "tailwind-merge"
3
 
4
  export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs))
6
  }
 
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
 
4
  export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
  }
next.config.ts CHANGED
@@ -1,14 +1,14 @@
1
  import type { NextConfig } from "next";
2
 
3
  const nextConfig: NextConfig = {
4
- output: 'standalone',
5
  async rewrites() {
6
  return [
7
  {
8
- source: '/api/:path*',
9
- destination: 'http://localhost:8000/api/:path*' // FastAPI backend
10
- }
11
- ]
12
  },
13
  };
14
 
 
1
  import type { NextConfig } from "next";
2
 
3
  const nextConfig: NextConfig = {
4
+ output: "standalone",
5
  async rewrites() {
6
  return [
7
  {
8
+ source: "/api/:path*",
9
+ destination: "http://localhost:8000/api/:path*", // FastAPI backend
10
+ },
11
+ ];
12
  },
13
  };
14
 
package.json CHANGED
@@ -10,11 +10,17 @@
10
  },
11
  "dependencies": {
12
  "@heroicons/react": "^2.1.5",
 
13
  "@radix-ui/react-dropdown-menu": "^2.1.2",
 
 
14
  "@radix-ui/react-slot": "^1.1.0",
 
15
  "ai": "^3.4.31",
16
  "class-variance-authority": "^0.7.0",
17
  "clsx": "^2.1.1",
 
 
18
  "lucide-react": "^0.456.0",
19
  "next": "15.0.2",
20
  "next-themes": "^0.4.3",
@@ -32,6 +38,7 @@
32
  "eslint": "^8",
33
  "eslint-config-next": "15.0.2",
34
  "postcss": "^8",
 
35
  "tailwindcss": "^3.4.14",
36
  "typescript": "^5"
37
  },
 
10
  },
11
  "dependencies": {
12
  "@heroicons/react": "^2.1.5",
13
+ "@radix-ui/react-dialog": "^1.1.2",
14
  "@radix-ui/react-dropdown-menu": "^2.1.2",
15
+ "@radix-ui/react-popover": "^1.1.2",
16
+ "@radix-ui/react-separator": "^1.1.0",
17
  "@radix-ui/react-slot": "^1.1.0",
18
+ "@tanstack/react-table": "^8.20.5",
19
  "ai": "^3.4.31",
20
  "class-variance-authority": "^0.7.0",
21
  "clsx": "^2.1.1",
22
+ "cmdk": "1.0.0",
23
+ "eslint-config-prettier": "^9.1.0",
24
  "lucide-react": "^0.456.0",
25
  "next": "15.0.2",
26
  "next-themes": "^0.4.3",
 
38
  "eslint": "^8",
39
  "eslint-config-next": "15.0.2",
40
  "postcss": "^8",
41
+ "prettier": "3.3.3",
42
  "tailwindcss": "^3.4.14",
43
  "typescript": "^5"
44
  },
pnpm-lock.yaml CHANGED
The diff for this file is too large to render. See raw diff
 
public/chatbots/code-tutor.webp ADDED
public/chatbots/whimsical.svg ADDED
tailwind.config.ts CHANGED
@@ -1,83 +1,83 @@
1
  import type { Config } from "tailwindcss";
2
 
3
  const config: Config = {
4
- darkMode: ['class', 'class'],
5
  content: [
6
  "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7
  "./components/**/*.{js,ts,jsx,tsx,mdx}",
8
  "./app/**/*.{js,ts,jsx,tsx,mdx}",
9
  ],
10
  theme: {
11
- extend: {
12
- colors: {
13
- primary: {
14
- DEFAULT: 'hsl(var(--primary))',
15
- dark: '#3DAA9D',
16
- light: '#7EDCD6',
17
- foreground: 'hsl(var(--primary-foreground))'
18
- },
19
- secondary: {
20
- DEFAULT: 'hsl(var(--secondary))',
21
- dark: '#E65555',
22
- light: '#FF8F8F',
23
- foreground: 'hsl(var(--secondary-foreground))'
24
- },
25
- accent: {
26
- blue: '#45B7D1',
27
- yellow: '#FDCB6E',
28
- dark: {
29
- blue: '#3A9BB2',
30
- yellow: '#D4A85D'
31
- },
32
- DEFAULT: 'hsl(var(--accent))',
33
- foreground: 'hsl(var(--accent-foreground))'
34
- },
35
- background: 'hsl(var(--background))',
36
- text: {
37
- primary: 'var(--text-primary)',
38
- secondary: 'var(--text-secondary)',
39
- tertiary: 'var(--text-tertiary)'
40
- },
41
- border: 'hsl(var(--border))',
42
- foreground: 'hsl(var(--foreground))',
43
- card: {
44
- DEFAULT: 'hsl(var(--card))',
45
- foreground: 'hsl(var(--card-foreground))'
46
- },
47
- popover: {
48
- DEFAULT: 'hsl(var(--popover))',
49
- foreground: 'hsl(var(--popover-foreground))'
50
- },
51
- muted: {
52
- DEFAULT: 'hsl(var(--muted))',
53
- foreground: 'hsl(var(--muted-foreground))'
54
- },
55
- destructive: {
56
- DEFAULT: 'hsl(var(--destructive))',
57
- foreground: 'hsl(var(--destructive-foreground))'
58
- },
59
- input: 'hsl(var(--input))',
60
- ring: 'hsl(var(--ring))',
61
- chart: {
62
- '1': 'hsl(var(--chart-1))',
63
- '2': 'hsl(var(--chart-2))',
64
- '3': 'hsl(var(--chart-3))',
65
- '4': 'hsl(var(--chart-4))',
66
- '5': 'hsl(var(--chart-5))'
67
- }
68
- },
69
- backdropBlur: {
70
- xs: '2px'
71
- },
72
- boxShadow: {
73
- dark: '0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.24)'
74
- },
75
- borderRadius: {
76
- lg: 'var(--radius)',
77
- md: 'calc(var(--radius) - 2px)',
78
- sm: 'calc(var(--radius) - 4px)'
79
- }
80
- }
81
  },
82
  plugins: [require("tailwindcss-animate")],
83
  };
 
1
  import type { Config } from "tailwindcss";
2
 
3
  const config: Config = {
4
+ darkMode: ["class", "class"],
5
  content: [
6
  "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7
  "./components/**/*.{js,ts,jsx,tsx,mdx}",
8
  "./app/**/*.{js,ts,jsx,tsx,mdx}",
9
  ],
10
  theme: {
11
+ extend: {
12
+ colors: {
13
+ primary: {
14
+ DEFAULT: "hsl(var(--primary))",
15
+ dark: "#3DAA9D",
16
+ light: "#7EDCD6",
17
+ foreground: "hsl(var(--primary-foreground))",
18
+ },
19
+ secondary: {
20
+ DEFAULT: "hsl(var(--secondary))",
21
+ dark: "#E65555",
22
+ light: "#FF8F8F",
23
+ foreground: "hsl(var(--secondary-foreground))",
24
+ },
25
+ accent: {
26
+ blue: "#45B7D1",
27
+ yellow: "#FDCB6E",
28
+ dark: {
29
+ blue: "#3A9BB2",
30
+ yellow: "#D4A85D",
31
+ },
32
+ DEFAULT: "hsl(var(--accent))",
33
+ foreground: "hsl(var(--accent-foreground))",
34
+ },
35
+ background: "hsl(var(--background))",
36
+ text: {
37
+ primary: "var(--text-primary)",
38
+ secondary: "var(--text-secondary)",
39
+ tertiary: "var(--text-tertiary)",
40
+ },
41
+ border: "hsl(var(--border))",
42
+ foreground: "hsl(var(--foreground))",
43
+ card: {
44
+ DEFAULT: "hsl(var(--card))",
45
+ foreground: "hsl(var(--card-foreground))",
46
+ },
47
+ popover: {
48
+ DEFAULT: "hsl(var(--popover))",
49
+ foreground: "hsl(var(--popover-foreground))",
50
+ },
51
+ muted: {
52
+ DEFAULT: "hsl(var(--muted))",
53
+ foreground: "hsl(var(--muted-foreground))",
54
+ },
55
+ destructive: {
56
+ DEFAULT: "hsl(var(--destructive))",
57
+ foreground: "hsl(var(--destructive-foreground))",
58
+ },
59
+ input: "hsl(var(--input))",
60
+ ring: "hsl(var(--ring))",
61
+ chart: {
62
+ "1": "hsl(var(--chart-1))",
63
+ "2": "hsl(var(--chart-2))",
64
+ "3": "hsl(var(--chart-3))",
65
+ "4": "hsl(var(--chart-4))",
66
+ "5": "hsl(var(--chart-5))",
67
+ },
68
+ },
69
+ backdropBlur: {
70
+ xs: "2px",
71
+ },
72
+ boxShadow: {
73
+ dark: "0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.24)",
74
+ },
75
+ borderRadius: {
76
+ lg: "var(--radius)",
77
+ md: "calc(var(--radius) - 2px)",
78
+ sm: "calc(var(--radius) - 4px)",
79
+ },
80
+ },
81
  },
82
  plugins: [require("tailwindcss-animate")],
83
  };