Bladeren bron

base version

master
azri 1 week geleden
bovenliggende
commit
adc27aacbe

+ 22
- 0
app/api/general/paymentService.ts Bestand weergeven

@@ -0,0 +1,22 @@
1
+import { PaymentType } from "@/types/payment";
2
+
3
+const dummyPaymentTypeData:PaymentType[] = [
4
+    {
5
+        id: 1,
6
+        type: "Card",
7
+        description: "Transfer via card number",
8
+        icon_id: "card"
9
+    },
10
+    {
11
+        id: 2,
12
+        type: "FPX",
13
+        description: "Transfer via card number",
14
+        icon_id: "bank"
15
+    }
16
+]
17
+
18
+export const getPaymentType = async ():Promise<PaymentType[] | undefined> => {
19
+
20
+    return dummyPaymentTypeData
21
+    
22
+}

+ 269
- 0
app/api/user/chatService.ts Bestand weergeven

@@ -0,0 +1,269 @@
1
+import type { Chat } from '@/types/chat';
2
+
3
+const dummyChatData: Chat[] = [
4
+    {
5
+        id: 1,
6
+        name: 'Sarah Badlishah',
7
+        persona: {
8
+            id: 1,
9
+            name: "Bella",
10
+            styles: [
11
+                {
12
+                    id: 1,
13
+                    name: "funny"
14
+                },
15
+                {
16
+                    id: 2,
17
+                    name: "energetic"
18
+                }
19
+            ],
20
+            role: "Sales Assistant",
21
+            active: true
22
+        },
23
+        chatList: [
24
+            {
25
+                self: true,
26
+                text: "saya nak buat payment",
27
+                time: "12:30",
28
+                date: "07/07/2025",
29
+                read: true
30
+            },
31
+            {
32
+                self: false,
33
+                text: "Selamat Datang !",
34
+                time: "12:30",
35
+                date: "07/07/2025",
36
+                read: true
37
+            },
38
+            {
39
+                self: false,
40
+                text: "Terima Kasih atas makluman! anda boleh buat bayaran melalui kaedah berikut:\n1. Online Transfer / FPX\n2. E-Wallet / QR Pay",
41
+                time: "12:30",
42
+                date: "07/07/2025",
43
+                read: true
44
+            },
45
+            {
46
+                self: true,
47
+                text: "Hello, boleh ke saya beli guna kad kredit ?",
48
+                time: "12:33",
49
+                date: "07/07/2025",
50
+                read: false
51
+            },
52
+            {
53
+                self: true,
54
+                text: "saya nak buat payment",
55
+                time: "12:30",
56
+                date: "07/07/2025",
57
+                read: true
58
+            },
59
+            {
60
+                self: false,
61
+                text: "Selamat Datang !",
62
+                time: "12:30",
63
+                date: "07/07/2025",
64
+                read: true
65
+            },
66
+            {
67
+                self: false,
68
+                text: "Terima Kasih atas makluman! anda boleh buat bayaran melalui kaedah berikut:\n1. Online Transfer / FPX\n2. E-Wallet / QR Pay",
69
+                time: "12:30",
70
+                date: "07/07/2025",
71
+                read: true
72
+            },
73
+            {
74
+                self: true,
75
+                text: "Hello, boleh ke saya beli guna kad kredit ?",
76
+                time: "12:33",
77
+                date: "07/07/2025",
78
+                read: false
79
+            }
80
+        ]
81
+    },
82
+    {
83
+        id: 2,
84
+        name: 'Haziq Imran',
85
+        persona: {
86
+            id: 2,
87
+            name: "Rafiq",
88
+            styles: [
89
+                {
90
+                    id: 2,
91
+                    name: "energetic"
92
+                },
93
+                {
94
+                    id: 3,
95
+                    name: "serious"
96
+                }
97
+            ],
98
+            role: "Sales Assistant",
99
+            active: false
100
+        },
101
+        chatList: [
102
+            {
103
+                self: true,
104
+                text: "saya dah buat bayaran, tolong semak ya",
105
+                time: "16:39",
106
+                date: "07/07/2025",
107
+                read: true
108
+            },
109
+            {
110
+                self: false,
111
+                text: "Baik, saya semak dahulu ya",
112
+                time: "16:40",
113
+                date: "07/07/2025",
114
+                read: true
115
+            },
116
+            {
117
+                self: false,
118
+                text: "Okay, bayaran anda telah diterima.",
119
+                time: "16:41",
120
+                date: "07/07/2025",
121
+                read: true
122
+            },
123
+            {
124
+                self: true,
125
+                text: "okay terima kasih",
126
+                time: "16:41",
127
+                date: "07/07/2025",
128
+                read: true
129
+            }
130
+        ]
131
+    },
132
+    {
133
+        id: 3,
134
+        name: 'Jimmy Tan',
135
+        persona: {
136
+            id: 3,
137
+            name: "Lina",
138
+            styles: [
139
+                {
140
+                    id: 3,
141
+                    name: "serious"
142
+                },
143
+                {
144
+                    id: 4,
145
+                    name: "professional"
146
+                }
147
+            ],
148
+            role: "HR Assistant",
149
+            active: true
150
+        },
151
+        chatList: [
152
+            {
153
+                self: true,
154
+                text: "how to refund this order?",
155
+                time: "08:35",
156
+                date: "07/07/2025",
157
+                read: true
158
+            },
159
+            {
160
+                self: false,
161
+                text: "Hi Jimmy! You can request a refund via our portal or contact our support. May I know your order ID?",
162
+                time: "08:36",
163
+                date: "07/07/2025",
164
+                read: true
165
+            },
166
+            {
167
+                self: true,
168
+                text: "Order ID is #54321",
169
+                time: "08:38",
170
+                date: "07/07/2025",
171
+                read: true
172
+            },
173
+            {
174
+                self: false,
175
+                text: "Noted, refund will be processed within 3 working days.",
176
+                time: "08:39",
177
+                date: "07/07/2025",
178
+                read: false
179
+            }
180
+        ]
181
+    },
182
+    {
183
+        id: 4,
184
+        name: 'Devaraj Elamko',
185
+        persona: {
186
+            id: 1,
187
+            name: "Bella",
188
+            styles: [
189
+                {
190
+                    id: 1,
191
+                    name: "funny"
192
+                },
193
+                {
194
+                    id: 2,
195
+                    name: "energetic"
196
+                }
197
+            ],
198
+            role: "Sales Assistant",
199
+            active: true
200
+        },
201
+        chatList: [
202
+            {
203
+                self: true,
204
+                text: "boleh ke jadikan order ni as a gift?",
205
+                time: "18:55",
206
+                date: "06/07/2025",
207
+                read: true
208
+            },
209
+            {
210
+                self: false,
211
+                text: "Ya boleh, cik boleh pilih 'Gift Option' semasa checkout.",
212
+                time: "18:56",
213
+                date: "06/07/2025",
214
+                read: true
215
+            }
216
+        ]
217
+    },
218
+    {
219
+        id: 5,
220
+        name: 'Nur Nadia',
221
+        persona: {
222
+            id: 3,
223
+            name: "Lina",
224
+            styles: [
225
+                {
226
+                    id: 3,
227
+                    name: "serious"
228
+                },
229
+                {
230
+                    id: 4,
231
+                    name: "professional"
232
+                }
233
+            ],
234
+            role: "HR Assistant",
235
+            active: true
236
+        },
237
+        chatList: [
238
+            {
239
+                self: true,
240
+                text: "saya nak order ni sampai hari jumaat ni",
241
+                time: "09:15",
242
+                date: "04/07/2025",
243
+                read: true
244
+            },
245
+            {
246
+                self: false,
247
+                text: "Noted! Kami akan usahakan penghantaran sebelum hari Jumaat.",
248
+                time: "09:16",
249
+                date: "04/07/2025",
250
+                read: true
251
+            },
252
+            {
253
+                self: true,
254
+                text: "Thank you!",
255
+                time: "09:17",
256
+                date: "04/07/2025",
257
+                read: true
258
+            }
259
+        ]
260
+    }
261
+];
262
+
263
+export const getAllChat = async (): Promise<Chat[]> => {
264
+    return dummyChatData
265
+}
266
+
267
+export const getChat = async (id: number): Promise<Chat | undefined> => {
268
+    return dummyChatData.find( chat => chat.id === id)
269
+}

+ 65
- 0
app/api/user/personaService.ts Bestand weergeven

@@ -0,0 +1,65 @@
1
+import type { Persona, PersonaStyle } from "@/types/persona"
2
+
3
+export const dummyPersonaStyle: PersonaStyle[] = [
4
+    {
5
+        id: 1,
6
+        name: "funny"
7
+    },
8
+    {
9
+        id: 2,
10
+        name: "energetic"
11
+    },
12
+    {
13
+        id: 3,
14
+        name: "serious"
15
+    },
16
+    {
17
+        id: 4,
18
+        name: "professional"
19
+    }
20
+]
21
+
22
+export const dummyPersonaData: Persona[] = [
23
+    {
24
+        id: 1,
25
+        name: "Bella",
26
+        styles: [
27
+            dummyPersonaStyle[0],
28
+            dummyPersonaStyle[1]
29
+        ],
30
+        role: "Sales Assistant",
31
+        active: true
32
+    },
33
+    {
34
+        id: 2,
35
+        name: "Rafiq",
36
+        styles: [
37
+            dummyPersonaStyle[1],
38
+            dummyPersonaStyle[3]
39
+        ],
40
+        role: "Sales Assistant",
41
+        active: false
42
+    },
43
+    {
44
+        id: 3,
45
+        name: "Lina",
46
+        styles: [
47
+            dummyPersonaStyle[2],
48
+            dummyPersonaStyle[3]
49
+        ],
50
+        role: "HR Assistant",
51
+        active: true
52
+    }
53
+]
54
+
55
+export const getPersona = async (): Promise<Persona[]> => {
56
+
57
+    return dummyPersonaData
58
+
59
+}
60
+
61
+export const getPersonaStyle = async (): Promise<PersonaStyle[]> => {
62
+
63
+    return dummyPersonaStyle
64
+
65
+}

+ 21
- 0
app/guest/layout.tsx Bestand weergeven

@@ -0,0 +1,21 @@
1
+// app/layout.tsx
2
+import 'antd/dist/reset.css' // or 'antd/dist/antd.css' for older versions
3
+import './globals.css'
4
+
5
+import type { Metadata } from 'next'
6
+
7
+import { Home, MessageCircle, User } from 'lucide-react'
8
+import Header from '@/components/layout/Header'
9
+
10
+export const metadata: Metadata = {
11
+  title: 'Mobile App',
12
+  description: 'Responsive app layout with sidebar',
13
+}
14
+
15
+export default function RootLayout({ children }: { children: React.ReactNode }) {
16
+
17
+  return (
18
+    <div className="w-full max-w-[430px] min-h-screen flex flex-col justify-between relative bg-white shadow-xl">
19
+    </div>
20
+  )
21
+}

+ 19
- 23
app/layout.tsx Bestand weergeven

@@ -1,33 +1,29 @@
1
-import type { Metadata } from "next";
2
-import { Geist, Geist_Mono } from "next/font/google";
3
-import "./globals.css";
1
+import './globals.css'
2
+import type { Metadata } from 'next'
4 3
 
5
-const geistSans = Geist({
6
-  variable: "--font-geist-sans",
7
-  subsets: ["latin"],
8
-});
4
+import { ConfigProvider } from 'antd'
5
+import type { ThemeConfig } from 'antd';
9 6
 
10
-const geistMono = Geist_Mono({
11
-  variable: "--font-geist-mono",
12
-  subsets: ["latin"],
13
-});
7
+import QueryProvider from '@/components/general/QueryProvider';
14 8
 
15
-export const metadata: Metadata = {
16
-  title: "Create Next App",
17
-  description: "Generated by create next app",
9
+const theme: ThemeConfig = {
10
+  // your theme config
18 11
 };
19 12
 
20
-export default function RootLayout({
21
-  children,
22
-}: Readonly<{
23
-  children: React.ReactNode;
24
-}>) {
13
+export const metadata: Metadata = {
14
+  title: 'Mobile App',
15
+  description: 'Responsive app layout with sidebar',
16
+}
17
+
18
+export default function RootLayout({ children }: { children: React.ReactNode }) {
25 19
   return (
26 20
     <html lang="en">
27
-      <body
28
-        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
29
-      >
30
-        {children}
21
+      <body className="bg-white flex justify-center relative">
22
+        <QueryProvider> {/* ✅ now inside client wrapper */}
23
+          <ConfigProvider theme={theme}>
24
+            {children}
25
+          </ConfigProvider>
26
+        </QueryProvider>
31 27
       </body>
32 28
     </html>
33 29
   );

+ 7
- 99
app/page.tsx Bestand weergeven

@@ -1,103 +1,11 @@
1
-import Image from "next/image";
1
+'use client'
2
+import { useEffect } from "react"
3
+
4
+export default function HomePage() {
2 5
 
3
-export default function Home() {
4
-  return (
5
-    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
6
-      <main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
7
-        <Image
8
-          className="dark:invert"
9
-          src="/next.svg"
10
-          alt="Next.js logo"
11
-          width={180}
12
-          height={38}
13
-          priority
14
-        />
15
-        <ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
16
-          <li className="mb-2 tracking-[-.01em]">
17
-            Get started by editing{" "}
18
-            <code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
19
-              app/page.tsx
20
-            </code>
21
-            .
22
-          </li>
23
-          <li className="tracking-[-.01em]">
24
-            Save and see your changes instantly.
25
-          </li>
26
-        </ol>
27 6
 
28
-        <div className="flex gap-4 items-center flex-col sm:flex-row">
29
-          <a
30
-            className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
31
-            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
32
-            target="_blank"
33
-            rel="noopener noreferrer"
34
-          >
35
-            <Image
36
-              className="dark:invert"
37
-              src="/vercel.svg"
38
-              alt="Vercel logomark"
39
-              width={20}
40
-              height={20}
41
-            />
42
-            Deploy now
43
-          </a>
44
-          <a
45
-            className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
46
-            href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
47
-            target="_blank"
48
-            rel="noopener noreferrer"
49
-          >
50
-            Read our docs
51
-          </a>
52
-        </div>
53
-      </main>
54
-      <footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
55
-        <a
56
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
57
-          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
58
-          target="_blank"
59
-          rel="noopener noreferrer"
60
-        >
61
-          <Image
62
-            aria-hidden
63
-            src="/file.svg"
64
-            alt="File icon"
65
-            width={16}
66
-            height={16}
67
-          />
68
-          Learn
69
-        </a>
70
-        <a
71
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
72
-          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
73
-          target="_blank"
74
-          rel="noopener noreferrer"
75
-        >
76
-          <Image
77
-            aria-hidden
78
-            src="/window.svg"
79
-            alt="Window icon"
80
-            width={16}
81
-            height={16}
82
-          />
83
-          Examples
84
-        </a>
85
-        <a
86
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
87
-          href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
88
-          target="_blank"
89
-          rel="noopener noreferrer"
90
-        >
91
-          <Image
92
-            aria-hidden
93
-            src="/globe.svg"
94
-            alt="Globe icon"
95
-            width={16}
96
-            height={16}
97
-          />
98
-          Go to nextjs.org →
99
-        </a>
100
-      </footer>
7
+  return (
8
+    <div className="space-y-6">
101 9
     </div>
102
-  );
10
+  )
103 11
 }

+ 92
- 0
app/user/chat/[chatID]/page.tsx Bestand weergeven

@@ -0,0 +1,92 @@
1
+'use client';
2
+import { useState } from 'react';
3
+import type { Chat } from '@/types/chat';
4
+import { useParams } from 'next/navigation'
5
+import { useQuery } from '@tanstack/react-query';
6
+import { getChat } from '@/app/api/user/chatService';
7
+
8
+const ChatPage: React.FC = () => {
9
+
10
+    const params = useParams()
11
+    const chatID = Number(params.chatID);
12
+
13
+    const {data:chat, error, isLoading} = useQuery<Chat | undefined>({
14
+        queryKey: ["getChat"],
15
+        queryFn: () => getChat(chatID)
16
+    })
17
+
18
+    const [enabled, setEnabled] = useState(false);
19
+
20
+    if(isLoading){
21
+        return <p>Loading...</p>
22
+    }
23
+
24
+    if(error){
25
+        return <p>Loading...</p>
26
+    }
27
+
28
+    return (
29
+        <div className="flex flex-col bg-gray-100 h-screen">
30
+            {/* Header */}
31
+            <div className="bg-white shadow p-4 flex flex-row">
32
+                <div>
33
+                    <img
34
+                        src="/default-avatar.png"
35
+                        alt="Profile"
36
+                        className="w-15 h-15 rounded-full object-cover border"
37
+                    />
38
+                </div>
39
+                <div className='ps-2'>
40
+                    <p className="text-xl text-gray-500">{}</p>
41
+                    <p className="text-md text-gray-500">{chat?.persona?.role}</p>
42
+                </div>
43
+                <div className='ps-2 flex flex-row gap-2 items-center ms-auto'>
44
+                    <span>Live</span>
45
+                    <button
46
+                        onClick={() => setEnabled(!enabled)}
47
+                        className={`w-12 h-6 flex items-center rounded-full p-1 transition-colors duration-300 ${enabled ? 'bg-blue-500' : 'bg-gray-300'
48
+                            }`}
49
+                    >
50
+                        <div
51
+                            className={`bg-white w-4 h-4 rounded-full shadow-md transform transition-transform duration-300 ${enabled ? 'translate-x-6' : 'translate-x-0'
52
+                                }`}
53
+                        />
54
+                    </button>
55
+                    <span>Offline</span>
56
+
57
+                </div>
58
+            </div>
59
+
60
+            {/* Chat messages */}
61
+            <div className="flex-1 overflow-y-auto px-4 py-6 space-y-4" style={{ maxHeight: "65vh" }}>
62
+                {chat?.chatList.map((msg, index) => (
63
+                    <div key={index}>
64
+                        <p className={`mb-1 text-gray-500 ${msg.self ? 'text-left' : 'text-right'}`}>
65
+                            {msg.self ? 'Visitor' : 'Ruccan Chat'}
66
+                        </p>
67
+                        <div className={`flex ${msg.self ? 'justify-start' : 'justify-end'}`}>
68
+                            <div className={`max-w-xs whitespace-pre-wrap px-4 py-2 rounded-lg text-sm ${msg.self ? 'bg-white text-gray-800 shadow' : 'bg-blue-500 text-white'}`}>
69
+                                {msg.text}
70
+                                <div className="text-[10px] mt-1 text-right opacity-70">
71
+                                    {msg.time}
72
+                                </div>
73
+                            </div>
74
+                        </div>
75
+                    </div>
76
+                ))}
77
+            </div>
78
+
79
+            {/* Input area */}
80
+            <div className="p-4 pb-0 bg-white border-t">
81
+                <input
82
+                    type="text"
83
+                    placeholder="Taip mesej..."
84
+                    className="w-full border rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
85
+                />
86
+            </div>
87
+        </div>
88
+
89
+    );
90
+};
91
+
92
+export default ChatPage;

+ 64
- 0
app/user/chat/page.tsx Bestand weergeven

@@ -0,0 +1,64 @@
1
+"use client";
2
+
3
+import React from 'react';
4
+import { Layout } from 'antd';
5
+import type { Chat } from '@/types/chat';
6
+import PageTitle from '@/components/ui/PageTitle';
7
+import { useRouter } from 'next/navigation';
8
+import { useQuery } from '@tanstack/react-query';
9
+import { getAllChat } from '@/app/api/user/chatService';
10
+
11
+const Chat: React.FC = () => {
12
+
13
+    const router = useRouter();
14
+    
15
+    const {data:chatList, error, isLoading} = useQuery<Chat[] | undefined | null>({
16
+        queryKey:["getAllChat"],
17
+        queryFn: () => getAllChat()
18
+    })
19
+
20
+    return (
21
+        <Layout>
22
+            <PageTitle title='RUCCAN CHAT' />
23
+            <div className="flex flex-col h-screen w-full bg-white">
24
+                {/* Chat List */}
25
+                {chatList?.map((chat) => {
26
+
27
+                    const lastChat = chat.chatList[chat.chatList.length - 1] 
28
+
29
+                    const unreadCount = chat.chatList.reduce((accum, chatItem) => {
30
+                        return chatItem.read ? accum : accum + 1;
31
+                    }, 0);
32
+
33
+                    return (
34
+                        <div
35
+                            key={chat.id}
36
+                            className="px-4 py-3 border-b border-gray-100 hover:bg-gray-50 cursor-pointer flex flex-col gap-1"
37
+                            onClick={()=>{
38
+                                router.push(`/user/chat/${chat.id}`)
39
+                            }}
40
+                        >
41
+                            <div className="flex justify-between items-center">
42
+                                <span className="font-medium text-gray-900">{chat.name}</span>
43
+                                <span className="text-xs text-gray-500">{lastChat.time}</span>
44
+                            </div>
45
+                            <div className="flex justify-between items-center">
46
+                                <p className="text-sm text-gray-600 truncate max-w-[250px]">
47
+                                    {lastChat.text}
48
+                                </p>
49
+                                {(unreadCount > 0) && (
50
+                                    <span className="bg-[#72c4ff] text-white text-xs font-bold px-2 py-0.5 rounded-full">
51
+                                        {unreadCount}
52
+                                    </span>
53
+                                )}
54
+
55
+                            </div>
56
+                        </div>
57
+                    )
58
+                })}
59
+            </div>
60
+        </Layout>
61
+    );
62
+};
63
+
64
+export default Chat;

+ 192
- 0
app/user/knowledge/create/page.tsx Bestand weergeven

@@ -0,0 +1,192 @@
1
+"use client";
2
+
3
+import { useState, useEffect } from 'react'
4
+import PageTitle from '@/components/ui/PageTitle';
5
+import { Upload, Select, Switch, Form, Button } from 'antd'
6
+import { UploadOutlined } from '@ant-design/icons';
7
+import InputList from '@/components/ui/InputList';
8
+import type { SelectProps } from 'antd';
9
+import TextArea from 'antd/es/input/TextArea';
10
+import { useQuery } from '@tanstack/react-query';
11
+import { getPersona } from '@/app/api/user/personaService';
12
+
13
+const CreatePersona: React.FC = () => {
14
+
15
+    const [name, setName] = useState<string | undefined>("")
16
+    const [isPremium, setIsPremium] = useState<boolean>(true)
17
+    const [options, setOptions] = useState<SelectProps[] | undefined>()
18
+    const [enabled, setEnabled] = useState({
19
+        isWebSearch: false,
20
+        isEcommerceShop: false,
21
+        isCanvas: false,
22
+        isDalleImageGeneration: false,
23
+        isCodeInterpreter: false
24
+    });
25
+
26
+    const { data: personas } = useQuery({
27
+        queryKey: ["getPersona"],
28
+        queryFn: () => getPersona()
29
+    })
30
+
31
+    const uploadProps = {
32
+        beforeUpload: (file: File) => {
33
+            console.log('File uploaded:', file);
34
+            return false; // Prevent automatic upload
35
+        },
36
+    };
37
+
38
+
39
+    useEffect(() => {
40
+        const latestData: SelectProps[] | undefined = personas?.map(persona => {
41
+            return {
42
+                label: persona.name,
43
+                value: persona.id
44
+            }
45
+        })
46
+        setOptions(latestData)
47
+    }, [personas])
48
+
49
+    return (
50
+        <div className="flex flex-col bg-white">
51
+            <PageTitle title='ADD KNOWLEDGE SOURCE' />
52
+            <div className='px-4 py-6'>
53
+                <Form layout="vertical">
54
+                    <Form.Item label={<p className='font-bold'>Select Persona</p>}>
55
+                        <Select
56
+                            allowClear
57
+                            style={{ width: '100%' }}
58
+                            placeholder="Select from list ..."
59
+                            options={options}
60
+                        />
61
+                    </Form.Item>
62
+
63
+                    <Form.Item label={<p className='font-bold'>Description</p>}>
64
+                        <TextArea
65
+                            placeholder='Add a short description about what Persona does'
66
+                            style={{ padding: 10 }}
67
+                            rows={2}
68
+                            value={name}
69
+                            onChange={(e) => setName(e.target.value)}
70
+                        />
71
+                    </Form.Item>
72
+
73
+                    <Form.Item label={<p className='font-bold'>Instruction</p>}>
74
+                        <TextArea
75
+                            placeholder='What does this Persona do?, How does it behave?, What should it avoid doing? '
76
+                            style={{ padding: 10 }}
77
+                            rows={4}
78
+                            value={name}
79
+                            onChange={(e) => setName(e.target.value)}
80
+                        />
81
+                    </Form.Item>
82
+
83
+                    <div className={`${isPremium ? 'visible' : 'hidden'}`}>
84
+                        <Form.Item
85
+                            label={
86
+                                <p className='font-bold'>
87
+                                    Capabilities
88
+                                    <span className='text-xs font-semibold px-2 py-1 bg-teal-200 rounded-full mx-2'>Premium</span>
89
+                                </p>
90
+                            }
91
+                        >
92
+                            <div className='flex flex-col gap-4'>
93
+
94
+                                {/* Web Search */}
95
+                                <div style={{maxHeight:"65px"}}>
96
+                                    <div className='flex items-center gap-2'>
97
+                                        <Switch
98
+                                            checked={enabled.isWebSearch}
99
+                                            onChange={(checked) =>
100
+                                                setEnabled({ ...enabled, isWebSearch: checked })
101
+                                            }
102
+                                        />
103
+                                        <span className='font-bold'>Web Search</span>
104
+                                    </div>
105
+                                    {enabled.isWebSearch && <InputList />}
106
+                                </div>
107
+
108
+                                {/* Commerce & Shop */}
109
+                                <div>
110
+                                    <div className='flex items-center gap-2'>
111
+                                        <Switch
112
+                                            checked={enabled.isEcommerceShop}
113
+                                            onChange={(checked) =>
114
+                                                setEnabled({ ...enabled, isEcommerceShop: checked })
115
+                                            }
116
+                                        />
117
+                                        <span className='font-bold'>Commerce & Shop</span>
118
+                                    </div>
119
+                                    {enabled.isEcommerceShop && (
120
+                                        <Upload {...uploadProps} className='!mt-2 block'>
121
+                                            <Button icon={<UploadOutlined />}>Upload Shop Catalog</Button>
122
+                                        </Upload>
123
+                                    )}
124
+                                </div>
125
+
126
+                                {/* Canvas */}
127
+                                <div>
128
+                                    <div className='flex items-center gap-2'>
129
+                                        <Switch
130
+                                            checked={enabled.isCanvas}
131
+                                            onChange={(checked) =>
132
+                                                setEnabled({ ...enabled, isCanvas: checked })
133
+                                            }
134
+                                        />
135
+                                        <span className='font-bold'>Canvas</span>
136
+                                    </div>
137
+                                    {enabled.isCanvas && (
138
+                                        <Upload {...uploadProps} className='!mt-2 block'>
139
+                                            <Button icon={<UploadOutlined />}>Upload Canvas File</Button>
140
+                                        </Upload>
141
+                                    )}
142
+                                </div>
143
+
144
+                                {/* DALL·E Image Generation */}
145
+                                <div>
146
+                                    <div className='flex items-center gap-2'>
147
+                                        <Switch
148
+                                            checked={enabled.isDalleImageGeneration}
149
+                                            onChange={(checked) =>
150
+                                                setEnabled({ ...enabled, isDalleImageGeneration: checked })
151
+                                            }
152
+                                        />
153
+                                        <span className='font-bold'>DALL-E Image Generation</span>
154
+                                    </div>
155
+                                    {enabled.isDalleImageGeneration && (
156
+                                        <Upload {...uploadProps} className='!mt-2 block'>
157
+                                            <Button icon={<UploadOutlined />}>Upload Prompt CSV</Button>
158
+                                        </Upload>
159
+                                    )}
160
+                                </div>
161
+
162
+                                {/* Code Interpreter */}
163
+                                <div>
164
+                                    <div className='flex items-center gap-2'>
165
+                                        <Switch
166
+                                            checked={enabled.isCodeInterpreter}
167
+                                            onChange={(checked) =>
168
+                                                setEnabled({ ...enabled, isCodeInterpreter: checked })
169
+                                            }
170
+                                        />
171
+                                        <span className='font-bold'>Code Interpreter & Data Analysis</span>
172
+                                    </div>
173
+                                    {enabled.isCodeInterpreter && (
174
+                                        <Upload {...uploadProps} className='!mt-2 block'>
175
+                                            <Button icon={<UploadOutlined />}>Upload Data File</Button>
176
+                                        </Upload>
177
+                                    )}
178
+                                </div>
179
+                            </div>
180
+                        </Form.Item>
181
+                    </div>
182
+
183
+                    <div className='text-center'>
184
+                        <Button type="primary" className='!w-fit !py-3 !text-xs !font-bold !px-10'>SUBMIT</Button>
185
+                    </div>
186
+                </Form>
187
+            </div>
188
+        </div>
189
+    )
190
+}
191
+
192
+export default CreatePersona;

+ 120
- 0
app/user/knowledge/page.tsx Bestand weergeven

@@ -0,0 +1,120 @@
1
+'use client';
2
+
3
+import React from 'react';
4
+import PageTitle from '@/components/ui/PageTitle';
5
+import {
6
+    EyeOutlined,
7
+    EditOutlined,
8
+    SortAscendingOutlined,
9
+    PlusCircleFilled
10
+} from '@ant-design/icons';
11
+
12
+type SourcePersonaItem = {
13
+    id: number;
14
+    persona_name: string;
15
+    knowledge_source: string[];
16
+}
17
+
18
+const sourcePersonas: SourcePersonaItem[] = [
19
+    {
20
+        id: 1,
21
+        persona_name: "Bella",
22
+        knowledge_source: ["Refund Policy", "Pricing SOP"],
23
+    },
24
+    {
25
+        id: 2,
26
+        persona_name: "Rafiq",
27
+        knowledge_source: ["Refund Policy", "Pricing SOP"],
28
+    },
29
+    {
30
+        id: 3,
31
+        persona_name: "Lina",
32
+        knowledge_source: ["Response Templates", "Customer FAQ"],
33
+    }
34
+];
35
+
36
+const Knowledge: React.FC = () => {
37
+    return (
38
+        <main className="flex flex-col bg-white h-full">
39
+            <PageTitle title="KNOWLEDGE SOURCES" />
40
+            {sourcePersonas.length > 0 ? <KnowledgeList /> : <EmptyKnowledgeIntro />}
41
+        </main>
42
+    );
43
+};
44
+
45
+const EmptyKnowledgeIntro: React.FC = () => {
46
+    return (
47
+        <div className="flex flex-col px-6 flex-1 justify-center items-center">
48
+            <div
49
+                className="w-full max-w-md text-center border-2 border-cyan-500 rounded-lg px-6 py-10 shadow-lg"
50
+                style={{ boxShadow: '0 10px 100px 30px rgba(59, 130, 246, 0.3)' }}
51
+            >
52
+                <p className="text-lg font-semibold mb-2 text-gray-800">NO SOURCE ADDED YET</p>
53
+                <p className="text-sm text-gray-600 mb-4">Get started by adding your first knowledge source</p>
54
+
55
+                <img
56
+                    src="/ruccan_logo.png"
57
+                    alt="Ruccan Logo"
58
+                    className="mx-auto mb-6"
59
+                    width={150}
60
+                    height={150}
61
+                />
62
+
63
+                <button className="w-fit rounded text-xs px-4 py-2 bg-[#369cc1] hover:bg-[#82c7d7] cursor-pointer text-white font-bold shadow transition-all duration-200">
64
+                    ADD FIRST SOURCE
65
+                </button>
66
+            </div>
67
+        </div>
68
+    );
69
+};
70
+
71
+const KnowledgeList: React.FC = () => {
72
+    return (
73
+        <div className="overflow-x-auto">
74
+            <div className="flex px-2 py-4">
75
+                <button className="w-fit rounded text-xs px-4 py-2 bg-[#369cc1] hover:bg-[#82c7d7] cursor-pointer text-white font-bold shadow transition-all duration-200 ms-auto">
76
+                    <PlusCircleFilled /> Add Source
77
+                </button>
78
+            </div>
79
+            <table className="w-full text-sm text-left text-gray-700 border-collapse">
80
+                <thead>
81
+                    <tr className="text-black">
82
+                        <th className="px-4 py-3 font-medium border border-gray-200">
83
+                            <div className="flex items-center gap-1 text-xs min-w-[80px]">
84
+                                AI persona <SortAscendingOutlined className="text-xs text-gray-400" />
85
+                            </div>
86
+                        </th>
87
+                        <th className="px-4 py-3 font-medium border border-gray-200">
88
+                            <div className="flex items-center gap-1 text-xs">
89
+                                Knowledge Sources <SortAscendingOutlined className="text-xs text-gray-400" />
90
+                            </div>
91
+                        </th>
92
+                        <th className="px-4 py-3 font-medium text-center border border-gray-200">Action</th>
93
+                    </tr>
94
+                </thead>
95
+                <tbody>
96
+                    {sourcePersonas.map((item) => (
97
+                        <tr key={item.id} className="border-t border-gray-200 hover:bg-gray-50">
98
+                            <td className="px-4 py-3">{item.persona_name}</td>
99
+                            <td className="px-4 py-3">
100
+                                {item.knowledge_source.join(', ')}
101
+                            </td>
102
+                            <td className="px-4 py-3 text-center">
103
+                                <div className="flex justify-center items-center gap-3 text-gray-600">
104
+                                    <button className="hover:text-blue-500" aria-label="Edit" title="Edit">
105
+                                        <EditOutlined />
106
+                                    </button>
107
+                                    <button className="hover:text-green-600" aria-label="View" title="View">
108
+                                        <EyeOutlined />
109
+                                    </button>
110
+                                </div>
111
+                            </td>
112
+                        </tr>
113
+                    ))}
114
+                </tbody>
115
+            </table>
116
+        </div>
117
+    );
118
+};
119
+
120
+export default Knowledge;

+ 58
- 0
app/user/layout.tsx Bestand weergeven

@@ -0,0 +1,58 @@
1
+'use client'
2
+
3
+import { Layout, Menu } from 'antd'
4
+import {
5
+  HomeOutlined,
6
+  MessageOutlined,
7
+  UserOutlined,
8
+} from '@ant-design/icons'
9
+import Header from '@/components/layout/Header'
10
+import { useRouter, usePathname } from 'next/navigation'
11
+
12
+const { Content } = Layout
13
+
14
+const AppLayout = ({ children }: { children: React.ReactNode }) => {
15
+  const router = useRouter()
16
+  const pathname = usePathname()
17
+
18
+  const navItems = [
19
+    {
20
+      key: '/user/dashboard',
21
+      icon: <HomeOutlined />,
22
+      label: 'Home',
23
+    },
24
+    {
25
+      key: '/user/chatboard',
26
+      icon: <MessageOutlined />,
27
+      label: 'Chat',
28
+    },
29
+    {
30
+      key: '/user/profile',
31
+      icon: <UserOutlined />,
32
+      label: 'Profile',
33
+    },
34
+  ]
35
+
36
+  return (
37
+    <Layout className="w-full max-w-[430px] min-h-screen mx-auto shadow-xl relative">
38
+      <Header />
39
+
40
+      <Content className="flex-1 overflow-auto bg-white pb-20" style={{minHeight:"93vh"}}>
41
+        {children}
42
+      </Content>
43
+
44
+      {/* Bottom Nav */}
45
+      <div className="fixed bottom-0 w-full max-w-[430px] mx-auto left-0 right-0 border-t border-gray-200 shadow-inner z-50 bg-white">
46
+        <Menu
47
+          mode="horizontal"
48
+          selectedKeys={[pathname]}
49
+          onClick={({ key }) => router.push(key)}
50
+          items={navItems}
51
+          className="flex justify-around"
52
+        />
53
+      </div>
54
+    </Layout>
55
+  )
56
+}
57
+
58
+export default AppLayout

+ 73
- 0
app/user/page.tsx Bestand weergeven

@@ -0,0 +1,73 @@
1
+"use client";
2
+
3
+import React from 'react'
4
+import { Flex, Typography, Divider, Layout, Button } from 'antd';
5
+import { RightOutlined, PhoneOutlined } from '@ant-design/icons';
6
+import SectionTitle from '@/components/ui/SectionTitle';
7
+import PageTitle from '@/components/ui/PageTitle';
8
+
9
+import LoadingMeter from '@/components/ui/LoadingMeter';
10
+
11
+const { Title, Text } = Typography;
12
+
13
+/* Main Dashboard */
14
+const page = () => {
15
+
16
+    return (
17
+        <section className="space-y-4">
18
+
19
+            <PageTitle title='DASHBOARD' />
20
+
21
+            <div className='px-4'>
22
+                <SectionTitle title='Plan Details' />
23
+
24
+                <Layout className='border-2 rounded border-cyan-500 px-4 py-2'>
25
+                    <div>
26
+                        <Text strong>Plan: </Text>
27
+                        <Text className='!text-blue-500'>Free Plan</Text>
28
+                    </div>
29
+
30
+                    <Text className='!text-xs mb-2'>
31
+                        This product is still in beta testing. There might be some issue, use at your own risk.
32
+                    </Text>
33
+
34
+                    <div className='mb-2'>
35
+                        <PhoneOutlined className='me-1' style={{ color: "red" }} />
36
+                        <Text strong className='!text-xs'>Contact: </Text>
37
+                        <Text className='!text-blue-500 !text-xs'>Aliff Akmal Bahri</Text>
38
+                    </div>
39
+
40
+                    <div className='!mb-2'>
41
+                        <Button className='!w-fit !rounded-full !text-xs !bg-[#b64fb1] !text-white'>Upgrade Plan <RightOutlined /> </Button>
42
+                    </div>
43
+
44
+                    <div className='border-2 rounded border-cyan-500 !bg-cyan-500 px-4 py-2 flex flex-row flex-wrap'>
45
+                        <div className='text-white'>
46
+                            <p className='text-xs font-bold mb-2'>Ruccan AI Labs </p>
47
+                            <p className='text-xs'>Explore our latest AI experiments </p>
48
+                            <p className='text-xs text-[#ebda5a]'>New tools added regularly!</p>
49
+                        </div>
50
+                        <div className='mx-auto text-center'>
51
+                            <img src={"/flask.png"} width="75" className='mb-2 mx-auto'/>
52
+                            <button className='w-fit rounded-lg text-xs px-4 py-2 bg-[#369cc1] hover:bg-[#436d86] text-white shadow border-0'>
53
+                                <span className='me-1'>Upgrade Plan</span> <RightOutlined />
54
+                            </button>
55
+                        </div>
56
+                    </div>
57
+
58
+                </Layout>
59
+
60
+                <SectionTitle title='Usage Limits' />
61
+
62
+                <Layout className='border-2 rounded border-cyan-500 px-4 py-2 bg-white'>
63
+                    <LoadingMeter title='AI Persona' progress={3} total={3} />
64
+                    <LoadingMeter title='Monthly Messages' progress={12} total={100} />
65
+                    <LoadingMeter title='Knowledge Base Scans' progress={1} total={10} />
66
+                </Layout>
67
+            </div>
68
+
69
+        </section>
70
+    )
71
+}
72
+
73
+export default page

+ 144
- 0
app/user/payment/page.tsx Bestand weergeven

@@ -0,0 +1,144 @@
1
+"use client";
2
+
3
+import { useState } from 'react'
4
+import PageTitle from '@/components/ui/PageTitle';
5
+import { Typography, Button, Input, Form, Select, Switch } from 'antd'
6
+import Link from 'next/link';
7
+import { BankFilled, WalletFilled } from '@ant-design/icons';
8
+import type { PaymentType } from '@/types/payment';
9
+import { useQuery } from '@tanstack/react-query';
10
+import { getPaymentType } from '@/app/api/general/paymentService';
11
+
12
+const { Text, Title } = Typography;
13
+
14
+const iconMapper = {
15
+  "card": <WalletFilled className='!text-white !text-xl' />,
16
+  "bank": <BankFilled className='!text-white !text-xl' />
17
+};
18
+
19
+const CreatePersona: React.FC = () => {
20
+  const [selectedPaymentID, setSelectedPaymentID] = useState<number | undefined>()
21
+  const [companyName, setCompanyName] = useState("")
22
+  const [cardNumber, setCardNumber] = useState("")
23
+  const [cvc, setCvc] = useState("")
24
+
25
+  const { data: paymentType, error, isLoading } = useQuery<PaymentType[] | undefined>({
26
+    queryKey: ["getPaymentType"],
27
+    queryFn: () => getPaymentType()
28
+  });
29
+
30
+  if (isLoading) {
31
+    return <p>Loading...</p>;
32
+  }
33
+
34
+  return (
35
+    <div className="flex flex-col bg-white">
36
+      <PageTitle title='PAYMENT' />
37
+      <div className='p-4 px-8 '>
38
+        <div className='flex flex-col gap-5 mb-8'>
39
+          <div className='text-xs'>
40
+            <p className='font-semibold'>Enter your payment details</p>
41
+            <p className='font-semibold italic'>
42
+              By continuing you agree to our <Link href="/">Terms</Link>
43
+            </p>
44
+          </div>
45
+        </div>
46
+
47
+        <div className='flex flex-row flex-wrap gap-2'>
48
+          {paymentType?.map(({ id, type, description, icon_id }) => (
49
+            <div
50
+              key={id}
51
+              className={`${selectedPaymentID === id ? 'bg-blue-950' : 'bg-gray-200'} hover:bg-blue-950 transition-all ease-in-out rounded-lg p-4 max-w-[160px] cursor-pointer`}
52
+              onClick={() => setSelectedPaymentID(id)}
53
+            >
54
+              {iconMapper[icon_id]}
55
+              <p className='text-white'>{`Transfer via card number`}</p>
56
+            </div>
57
+          ))}
58
+        </div>
59
+
60
+        <div className="px-2 mt-4">
61
+          <Form layout="vertical">
62
+            <div className="grid grid-cols-1 sm:grid-cols-2">
63
+              <Form.Item label={<Text className='!italic !text-xs !font-semibold'>Company Name</Text>}>
64
+                <Input
65
+                  placeholder="Company Name"
66
+                  value={companyName}
67
+                  onChange={(e) => setCompanyName(e.target.value)}
68
+                />
69
+              </Form.Item>
70
+
71
+              <Form.Item label={<Text className='!italic !text-xs !font-semibold'>Card Number</Text>}>
72
+                <Input
73
+                  value={cardNumber}
74
+                  onChange={(e) => {
75
+                    const rawValue = e.target.value.replace(/\D/g, '').slice(0, 16);
76
+                    const formattedValue = rawValue.replace(/(.{4})/g, '$1 ').trim();
77
+                    setCardNumber(formattedValue);
78
+                  }}
79
+                />
80
+              </Form.Item>
81
+
82
+              <div className='flex flex-row gap-5'>
83
+                <Form.Item label={<Text className='!italic !text-xs !font-semibold'>Exp Month</Text>} className="w-full">
84
+                  <Select
85
+                    allowClear
86
+                    style={{ width: '100%' }}
87
+                    placeholder="Please select"
88
+                    options={
89
+                      Array.from({ length: 12 }, (_, i) => {
90
+                        const month = String(i + 1).padStart(2, '0');
91
+                        return { label: month, value: month };
92
+                      })
93
+                    }
94
+                  />
95
+                </Form.Item>
96
+
97
+                <Form.Item label={<Text className='!italic !text-xs !font-semibold'>Exp Year</Text>} className="w-full">
98
+                  <Select
99
+                    allowClear
100
+                    style={{ width: '100%' }}
101
+                    placeholder="Please select"
102
+                    options={
103
+                      Array.from({ length: 10 }, (_, i) => {
104
+                        const year = new Date().getFullYear() + i;
105
+                        return { label: String(year), value: String(year) };
106
+                      })
107
+                    }
108
+                  />
109
+                </Form.Item>
110
+              </div>
111
+
112
+              <Form.Item label={<Text className='!italic !text-xs !font-semibold'>CVC</Text>}>
113
+                <div className='flex gap-6 items-center'>
114
+                  <Input
115
+                    placeholder="123"
116
+                    className='!max-w-[50px]'
117
+                    value={cvc}
118
+                    onChange={(e) => {
119
+                      const rawValue = e.target.value.replace(/\D/g, '').slice(0, 4);
120
+                      setCvc(rawValue);
121
+                    }}
122
+                  />
123
+                  <p className='text-gray-300 max-w-[160px]' style={{ fontSize: 10 }}>
124
+                    3 or 4 digits usually found on the signature strips
125
+                  </p>
126
+                </div>
127
+              </Form.Item>
128
+
129
+              <Form.Item>
130
+                <div className='flex flex-row items-center gap-2'>
131
+                  <Switch checked={true} onChange={() => { }} />
132
+                  <Text className='!text-xs font-semibold'>SET AS DEFAULT</Text>
133
+                </div>
134
+              </Form.Item>
135
+            </div>
136
+            <Button type="primary" className='!w-full !py-3'>CONFIRM</Button>
137
+          </Form>
138
+        </div>
139
+      </div>
140
+    </div>
141
+  )
142
+}
143
+
144
+export default CreatePersona;

+ 193
- 0
app/user/persona/create/page.tsx Bestand weergeven

@@ -0,0 +1,193 @@
1
+"use client";
2
+
3
+import { useState, useEffect } from 'react'
4
+import PageTitle from '@/components/ui/PageTitle';
5
+import { Input, Typography, Select, Switch, Upload, Form, Button } from 'antd'
6
+import { UploadOutlined } from '@ant-design/icons';
7
+import type { SelectProps } from 'antd';
8
+import TextArea from 'antd/es/input/TextArea';
9
+import { useQuery } from '@tanstack/react-query';
10
+import { getPersonaStyle } from '@/app/api/user/personaService';
11
+import type { RcFile } from 'antd/es/upload';
12
+
13
+
14
+const CreatePersona: React.FC = () => {
15
+
16
+
17
+    const [name, setName] = useState<string | undefined>("")
18
+    const [options, setOptions] = useState<SelectProps[] | undefined>()
19
+    const [imageUrl, setImageUrl] = useState<string | null>(null);
20
+    const [fileList, setFileList] = useState<any[]>([]);
21
+    const [enabled, setEnabled] = useState({
22
+        isManualKnowledgeEntry: false,
23
+        isUploadTextDocument: false,
24
+        isImportWebLink: false,
25
+        isCodeInterpreter: false
26
+    });
27
+
28
+    const { data: personaStyleList, error, isLoading } = useQuery({
29
+        queryKey: ["personaStyle"],
30
+        queryFn: () => getPersonaStyle()
31
+    })
32
+
33
+    useEffect(() => {
34
+
35
+        const latestData: SelectProps[] | undefined = personaStyleList?.map(persona => {
36
+            return {
37
+                label: persona.name,
38
+                value: persona.id
39
+            }
40
+        })
41
+
42
+        setOptions(latestData)
43
+
44
+    }, [personaStyleList])
45
+
46
+    const handleBeforeUpload = (file: RcFile) => {
47
+        const isImage = file.type.startsWith('image/');
48
+        if (!isImage) {
49
+            // message.error('Only image files are allowed!');
50
+        }
51
+
52
+        const isLt2M = file.size / 1024 / 1024 < 2;
53
+        if (!isLt2M) {
54
+            //message.error('Image must be smaller than 2MB!');
55
+        }
56
+
57
+        if (isImage && isLt2M) {
58
+            const reader = new FileReader();
59
+            reader.onload = () => setImageUrl(reader.result as string);
60
+            reader.readAsDataURL(file);
61
+        }
62
+
63
+        // prevent automatic upload
64
+        return false;
65
+    };
66
+
67
+
68
+    return (
69
+        <div className="flex flex-col bg-white">
70
+            <PageTitle title='CREATE PERSONA' />
71
+            <div className='px-4 py-6'>
72
+                <Form layout="vertical">
73
+                    <Form.Item label="Profile Name">
74
+                        <Input
75
+                            style={{ padding: 10 }}
76
+                            value={name}
77
+                            onChange={(e) => setName(e.target.value)}
78
+                        />
79
+                    </Form.Item>
80
+
81
+                    <Form.Item label="Persona Image">
82
+                        <Upload
83
+                            beforeUpload={handleBeforeUpload}
84
+                            fileList={fileList}
85
+                            onRemove={() => {
86
+                                setFileList([]);
87
+                                setImageUrl(null);
88
+                            }}
89
+                            onChange={({ fileList }) => setFileList(fileList)}
90
+                            maxCount={1}
91
+                            accept="image/*"
92
+                            listType="picture"
93
+                        >
94
+                            <button className='border border-gray-300 px-4 py-2 rounded hover:bg-gray-50'>
95
+                                <UploadOutlined /> Click to Upload
96
+                            </button>
97
+                        </Upload>
98
+                        {imageUrl && (
99
+                            <img
100
+                                src={imageUrl}
101
+                                alt="Uploaded Preview"
102
+                                className="mt-2 w-32 h-32 object-cover border rounded"
103
+                            />
104
+                        )}
105
+                    </Form.Item>
106
+
107
+                    <Form.Item label="Role / Department">
108
+                        <Input
109
+                            style={{ padding: 10 }}
110
+                            value={name}
111
+                            onChange={(e) => setName(e.target.value)}
112
+                        />
113
+                    </Form.Item>
114
+
115
+                    <Form.Item label="Description">
116
+                        <TextArea
117
+                            style={{ padding: 10 }}
118
+                            rows={6}
119
+                            value={name}
120
+                            onChange={(e) => setName(e.target.value)}
121
+                        />
122
+                    </Form.Item>
123
+
124
+                    <Form.Item label="Persona Style">
125
+                        <Select
126
+                            mode="multiple"
127
+                            allowClear
128
+                            style={{ width: '100%' }}
129
+                            placeholder="Please select"
130
+                            options={options}
131
+                        />
132
+                    </Form.Item>
133
+
134
+                    <Form.Item label="Greeting Message">
135
+                        <TextArea
136
+                            style={{ padding: 10 }}
137
+                            rows={3}
138
+                            value={name}
139
+                            onChange={(e) => setName(e.target.value)}
140
+                        />
141
+                    </Form.Item>
142
+
143
+                    <Form.Item label="Persona Training Center">
144
+                        <div className='flex flex-col gap-2'>
145
+                            <div className='flex items-center gap-2'>
146
+                                <Switch
147
+                                    checked={enabled.isManualKnowledgeEntry}
148
+                                    onChange={(checked) =>
149
+                                        setEnabled({ ...enabled, isManualKnowledgeEntry: checked })
150
+                                    }
151
+                                />
152
+                                <span className='font-bold'>Manual Knowledge Entry</span>
153
+                            </div>
154
+                            <div className='flex items-center gap-2'>
155
+                                <Switch
156
+                                    checked={enabled.isUploadTextDocument}
157
+                                    onChange={(checked) =>
158
+                                        setEnabled({ ...enabled, isUploadTextDocument: checked })
159
+                                    }
160
+                                />
161
+                                <span className='font-bold'>Upload Text Document</span>
162
+                            </div>
163
+                            <div className='flex items-center gap-2'>
164
+                                <Switch
165
+                                    checked={enabled.isImportWebLink}
166
+                                    onChange={(checked) =>
167
+                                        setEnabled({ ...enabled, isImportWebLink: checked })
168
+                                    }
169
+                                />
170
+                                <span className='font-bold'>Import from Web Link</span>
171
+                            </div>
172
+                            <div className='flex items-center gap-2'>
173
+                                <Switch
174
+                                    checked={enabled.isCodeInterpreter}
175
+                                    onChange={(checked) =>
176
+                                        setEnabled({ ...enabled, isCodeInterpreter: checked })
177
+                                    }
178
+                                />
179
+                                <span className='font-bold'>Code Interpreter & Data Analysis</span>
180
+                            </div>
181
+                        </div>
182
+                    </Form.Item>
183
+                    <div className='text-center'>
184
+                        <Button type="primary" className='!w-fit !py-3 !text-xs !font-bold !px-10'>SUBMIT</Button>
185
+                    </div>
186
+                </Form>
187
+            </div>
188
+        </div>
189
+
190
+    )
191
+}
192
+
193
+export default CreatePersona

+ 139
- 0
app/user/persona/page.tsx Bestand weergeven

@@ -0,0 +1,139 @@
1
+'use client';
2
+
3
+import React, {useEffect, useState} from 'react';
4
+import PageTitle from '@/components/ui/PageTitle';
5
+import {
6
+    EyeOutlined,
7
+    EditOutlined,
8
+    SortAscendingOutlined,
9
+    PlusCircleFilled
10
+} from '@ant-design/icons';
11
+import type { Persona } from '@/types/persona';
12
+import { getPersona } from '@/app/api/user/personaService';
13
+import { useQuery } from '@tanstack/react-query';
14
+
15
+interface PersonaListProps {
16
+  personaData: Persona[] | undefined;
17
+}
18
+
19
+const PersonaPage: React.FC = () => {
20
+    
21
+
22
+    let {data:personas, error, isLoading} = useQuery<Persona[]>({
23
+        queryKey:["getPersona"],
24
+        queryFn: () => getPersona()
25
+    })
26
+
27
+    if(isLoading) {
28
+        return <p>Loading...</p>
29
+    }
30
+    
31
+    if(!personas) {
32
+        return <p></p>
33
+    }
34
+
35
+    return (
36
+        <main className="flex flex-col bg-white h-full">
37
+            <PageTitle title="PERSONA" />
38
+            {(personas.length > 0) ? <PersonaList personaData={personas} /> : <PersonaIntro />}
39
+        </main>
40
+    );
41
+};
42
+
43
+const PersonaIntro: React.FC = () => {
44
+    return (
45
+        <div className="flex flex-col px-6 flex-1 justify-center items-center">
46
+            <div
47
+                className="w-full max-w-md text-center border-2 border-cyan-500 rounded-lg px-6 py-10 shadow-lg"
48
+                style={{ boxShadow: '0 10px 100px 30px rgba(59, 130, 246, 0.3)' }} // red-500 with opacity
49
+            >
50
+                <p className="text-lg font-semibold mb-2 text-gray-800">NO PERSONA CREATED YET</p>
51
+                <p className="text-sm text-gray-600 mb-4">Get started by creating your first AI Persona</p>
52
+
53
+                <img
54
+                    src="/ruccan_logo.png"
55
+                    alt="Ruccan Logo"
56
+                    className="mx-auto mb-6"
57
+                    width={150}
58
+                    height={150}
59
+                />
60
+
61
+                <button className="w-fit rounded text-xs px-4 py-2 bg-[#369cc1] hover:bg-[#82c7d7] cursor-pointer text-white font-bold shadow transition-all duration-200">
62
+                    CREATE YOUR FIRST PERSONA
63
+                </button>
64
+            </div>
65
+        </div>
66
+    )
67
+}
68
+
69
+const PersonaList: React.FC<PersonaListProps> = ({personaData}) => {
70
+    return (
71
+        <div className="overflow-x-auto">
72
+            <div className='flex px-2 py-4'>
73
+                <button className="w-fit rounded text-xs px-4 py-2 bg-[#369cc1] hover:bg-[#82c7d7] cursor-pointer text-white font-bold shadow transition-all duration-200 ms-auto">
74
+                <PlusCircleFilled /> Create New Persona
75
+            </button>
76
+            </div>
77
+            <table className="w-full text-sm text-left text-gray-700 border-collapse">
78
+                <thead>
79
+                    <tr className="bg-gray-100 text-gray-700">
80
+                        <th className="px-4 py-3 font-medium">
81
+                            <div className="flex items-center gap-1">
82
+                                Name <SortAscendingOutlined className="text-xs text-gray-400" />
83
+                            </div>
84
+                        </th>
85
+                        <th className="px-4 py-3 font-medium">
86
+                            <div className="flex items-center gap-1">
87
+                                Role <SortAscendingOutlined className="text-xs text-gray-400" />
88
+                            </div>
89
+                        </th>
90
+                        <th className="px-4 py-3 font-medium">
91
+                            <div className="flex items-center gap-1">
92
+                                Status <SortAscendingOutlined className="text-xs text-gray-400" />
93
+                            </div>
94
+                        </th>
95
+                        <th className="px-4 py-3 font-medium text-center">Action</th>
96
+                    </tr>
97
+                </thead>
98
+                <tbody>
99
+                    {personaData?.map((persona) => (
100
+                        <tr key={persona.id} className="border-t border-gray-200 hover:bg-gray-50">
101
+                            <td className="px-4 py-3">{persona.name}</td>
102
+                            <td className="px-4 py-3">{persona.role}</td>
103
+                            <td className="px-4 py-3">
104
+                                <span
105
+                                    className={`px-2 py-1 rounded-full text-xs font-semibold ${persona.active
106
+                                        ? 'bg-green-100 text-green-600'
107
+                                        : 'bg-amber-600 text-white'
108
+                                        }`}
109
+                                >
110
+                                    {persona.active ? 'Active' : 'Inactive'}
111
+                                </span>
112
+                            </td>
113
+                            <td className="px-4 py-3 text-center">
114
+                                <div className="flex justify-center items-center gap-3 text-gray-600">
115
+                                    <button
116
+                                        className="hover:text-blue-500"
117
+                                        aria-label="Edit"
118
+                                        title="Edit"
119
+                                    >
120
+                                        <EditOutlined />
121
+                                    </button>
122
+                                    <button
123
+                                        className="hover:text-green-600"
124
+                                        aria-label="View"
125
+                                        title="View"
126
+                                    >
127
+                                        <EyeOutlined />
128
+                                    </button>
129
+                                </div>
130
+                            </td>
131
+                        </tr>
132
+                    ))}
133
+                </tbody>
134
+            </table>
135
+        </div>
136
+    );
137
+};
138
+
139
+export default PersonaPage;

+ 209
- 0
app/user/pricing/page.tsx Bestand weergeven

@@ -0,0 +1,209 @@
1
+"use client";
2
+
3
+import { useState, useEffect } from 'react'
4
+import PageTitle from '@/components/ui/PageTitle';
5
+import { Typography, Button } from 'antd'
6
+import { CheckCircleOutlined, XOutlined } from '@ant-design/icons';
7
+
8
+const { Text, Title } = Typography;
9
+
10
+const basicPlan = [
11
+    {
12
+        available: true,
13
+        text: "Manual Knowledge Entry"
14
+    },
15
+    {
16
+        available: true,
17
+        text: "Upload 5 Text Docs/months"
18
+    },
19
+    {
20
+        available: false,
21
+        text: "No Web Link Import"
22
+    },
23
+    {
24
+        available: false,
25
+        text: "No Image-to-text OCR"
26
+    },
27
+    {
28
+        available: false,
29
+        text: "No SOP Auto-Learning"
30
+    },
31
+    {
32
+        available: true,
33
+        text: "Max 10 Knowledge Sources"
34
+    },
35
+    {
36
+        available: true,
37
+        text: "Email Support"
38
+    }
39
+]
40
+
41
+const standardPlan = [
42
+    {
43
+        available: true,
44
+        text: "Manual Knowledge Entry"
45
+    },
46
+    {
47
+        available: true,
48
+        text: "Upload 5 Text Docs/months"
49
+    },
50
+    {
51
+        available: true,
52
+        text: "No Web Link Import"
53
+    },
54
+    {
55
+        available: true,
56
+        text: "No Image-to-text OCR"
57
+    },
58
+    {
59
+        available: false,
60
+        text: "No SOP Auto-Learning"
61
+    },
62
+    {
63
+        available: true,
64
+        text: "Max 10 Knowledge Sources"
65
+    },
66
+    {
67
+        available: true,
68
+        text: "Email Support"
69
+    }
70
+]
71
+
72
+const premiumPlan = [
73
+    {
74
+        available: true,
75
+        text: "Manual Knowledge Entry"
76
+    },
77
+    {
78
+        available: true,
79
+        text: "Upload 5 Text Docs/months"
80
+    },
81
+    {
82
+        available: true,
83
+        text: "No Web Link Import"
84
+    },
85
+    {
86
+        available: true,
87
+        text: "No Image-to-text OCR"
88
+    },
89
+    {
90
+        available: true,
91
+        text: "No SOP Auto-Learning"
92
+    },
93
+    {
94
+        available: true,
95
+        text: "Max 10 Knowledge Sources"
96
+    },
97
+    {
98
+        available: true,
99
+        text: "Email Support"
100
+    }
101
+]
102
+
103
+const CreatePersona: React.FC = () => {
104
+
105
+
106
+    return (
107
+        <div className="flex flex-col bg-white">
108
+            <PageTitle title='PREMIUM PLAN' />
109
+            <div className='p-4 px-8 flex flex-col gap-5'>
110
+                <div className='border rounded-lg border-black flex flex-col overflow-hidden'>
111
+                    <div className='flex flex-col py-2 px-3'>
112
+                        <Title level={5}>Basic</Title>
113
+                        <div className='flex flex-row justify-between'>
114
+                            <div className='flex flex-row items-center gap-3'>
115
+                                <Title level={3} className='!m-0'>$5</Title>
116
+                                <div>
117
+                                    <p className='p-0 text-xs font-semibold'>Per agent</p>
118
+                                    <p className='p-0 text-xs font-semibold'>Per month</p>
119
+                                </div>
120
+                            </div>
121
+                            <div>
122
+                                <Button type='primary' className='!px-10 !font-bold'>Select</Button>
123
+                            </div>
124
+                        </div>
125
+                    </div>
126
+
127
+                    <div className='text-center border-y border-gray-200'>
128
+                        <p>ideal for simple, entry-level AI personas</p>
129
+                    </div>
130
+
131
+                    <div className='flex flex-col py-2 px-3 ps-10'>
132
+                        {basicPlan.map(({available, text}) => (
133
+                            <div>
134
+                                {(available) ? <CheckCircleOutlined className='!text-xs !align-middle !text-blue-800' /> : <XOutlined className='!text-xs !align-middle !text-red-800' />}
135
+                                <span className="ml-1 align-middle text-xs font-semibold">{text}</span>
136
+                            </div>
137
+                        ))}
138
+
139
+                    </div>
140
+                </div>
141
+                <div className='border rounded-lg border-black flex flex-col overflow-hidden'>
142
+                    <div className='flex flex-col py-2 px-3'>
143
+                        <Title level={5}>Standard</Title>
144
+                        <div className='flex flex-row justify-between'>
145
+                            <div className='flex flex-row items-center gap-3'>
146
+                                <Title level={3} className='!m-0'>$15</Title>
147
+                                <div>
148
+                                    <p className='p-0 text-xs font-semibold'>Per agent</p>
149
+                                    <p className='p-0 text-xs font-semibold'>Per month</p>
150
+                                </div>
151
+                            </div>
152
+                            <div>
153
+                                <Button type='primary' className='!px-10 !font-bold'>Select</Button>
154
+                            </div>
155
+                        </div>
156
+                    </div>
157
+
158
+                    <div className='text-center border-y border-gray-200'>
159
+                        <p>Perfect for growing AI needs</p>
160
+                    </div>
161
+
162
+                    <div className='flex flex-col py-2 px-3 ps-10'>
163
+                        {standardPlan.map(({available, text}) => (
164
+                            <div>
165
+                                {(available) ? <CheckCircleOutlined className='!text-xs !align-middle !text-blue-800' /> : <XOutlined className='!text-xs !align-middle !text-red-800' />}
166
+                                <span className="ml-1 align-middle text-xs font-semibold">{text}</span>
167
+                            </div>
168
+                        ))}
169
+
170
+                    </div>
171
+                </div>
172
+                <div className='border rounded-lg border-black flex flex-col overflow-hidden'>
173
+                    <div className='flex flex-col py-2 px-3'>
174
+                        <Title level={5}>Premium</Title>
175
+                        <div className='flex flex-row justify-between'>
176
+                            <div className='flex flex-row items-center gap-3'>
177
+                                <Title level={3} className='!m-0'>$45</Title>
178
+                                <div>
179
+                                    <p className='p-0 text-xs font-semibold'>Per agent</p>
180
+                                    <p className='p-0 text-xs font-semibold'>Per month</p>
181
+                                </div>
182
+                            </div>
183
+                            <div>
184
+                                <Button type='primary' className='!px-10 !font-bold'>Select</Button>
185
+                            </div>
186
+                        </div>
187
+                    </div>
188
+
189
+                    <div className='text-center border-y border-gray-200'>
190
+                        <p>Perfect for large enterprise AI model</p>
191
+                    </div>
192
+
193
+                    <div className='flex flex-col py-2 px-3 ps-10'>
194
+                        {premiumPlan.map(({available, text}) => (
195
+                            <div>
196
+                                {(available) ? <CheckCircleOutlined className='!text-xs !align-middle !text-blue-800' /> : <XOutlined className='!text-xs !align-middle !text-red-800' />}
197
+                                <span className="ml-1 align-middle text-xs font-semibold">{text}</span>
198
+                            </div>
199
+                        ))}
200
+
201
+                    </div>
202
+                </div>
203
+            </div>
204
+        </div>
205
+
206
+    )
207
+}
208
+
209
+export default CreatePersona

+ 135
- 0
app/user/profile/page.tsx Bestand weergeven

@@ -0,0 +1,135 @@
1
+'use client'
2
+
3
+import {
4
+    UploadOutlined,
5
+    DeleteOutlined
6
+} from '@ant-design/icons'
7
+import { Button, Input, Typography, Upload, Form } from 'antd'
8
+import { useState } from 'react'
9
+
10
+const { Title, Text } = Typography
11
+
12
+const ProfileSettings = () => {
13
+    const [companyName, setCompanyName] = useState('')
14
+    const [role, setRole] = useState('')
15
+    const [profileName, setProfileName] = useState('')
16
+    const [newPassword, setNewPassword] = useState('')
17
+    const [confirmPassword, setConfirmPassword] = useState('')
18
+
19
+    return (
20
+        <Form layout="vertical" className="!flex !flex-col !gap-y-8 !max-w-2xl !mx-auto !p-5">
21
+            {/* 1. Profile Photo */}
22
+            <div>
23
+                <p className="font-bold text-white bg-pink-300 py-1 px-5 w-fit rounded-full">
24
+                    Profile Photo
25
+                </p>
26
+                <div className="flex flex-col items-center gap-4 mt-4">
27
+                    <img
28
+                        src="/default-avatar.png"
29
+                        alt="Profile"
30
+                        className="w-20 h-20 rounded-full object-cover border"
31
+                    />
32
+                    <Upload showUploadList={false}>
33
+                        <Button type='primary'>
34
+                            <span className='font-bold'>Upload Photo</span>
35
+                        </Button>
36
+                    </Upload>
37
+                </div>
38
+            </div>
39
+
40
+            {/* 2. Company Info */}
41
+            <div>
42
+                <p className="font-bold text-white bg-pink-300 py-1 px-5 w-fit rounded-full">
43
+                    Company Info
44
+                </p>
45
+                <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
46
+                    <Form.Item label={<Title level={5}>Company Name</Title>}>
47
+                        <Input
48
+                            placeholder="Company Name"
49
+                            value={companyName}
50
+                            onChange={(e) => setCompanyName(e.target.value)}
51
+                        />
52
+                    </Form.Item>
53
+                    <Form.Item label={<Title level={5}>My Role</Title>}>
54
+                        <Input
55
+                            placeholder="My Role"
56
+                            value={role}
57
+                            onChange={(e) => setRole(e.target.value)}
58
+                        />
59
+                    </Form.Item>
60
+                </div>
61
+            </div>
62
+
63
+            {/* 3. Profile Name */}
64
+            <div>
65
+                <p className="font-bold text-white bg-pink-300 py-1 px-5 w-fit rounded-full">
66
+                    Profile Name
67
+                </p>
68
+                <div className="mt-4">
69
+                    <Form.Item label={<Title level={5}>Profile Name</Title>}>
70
+                        <Input
71
+                            placeholder="Profile Name"
72
+                            value={profileName}
73
+                            onChange={(e) => setProfileName(e.target.value)}
74
+                        />
75
+                    </Form.Item>
76
+                </div>
77
+            </div>
78
+
79
+            {/* 4. Change Password */}
80
+            <div>
81
+                <p className="font-bold text-white bg-pink-300 py-1 px-5 w-fit rounded-full">
82
+                    Change Password
83
+                </p>
84
+                <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
85
+                    <Form.Item label={<Title level={5}>New Password</Title>}>
86
+                        <Input.Password
87
+                            placeholder="New Password"
88
+                            value={newPassword}
89
+                            onChange={(e) => setNewPassword(e.target.value)}
90
+                        />
91
+                    </Form.Item>
92
+                    <Form.Item label={<Title level={5}>Confirm Password</Title>}>
93
+                        <Input.Password
94
+                            placeholder="Confirm Password"
95
+                            value={confirmPassword}
96
+                            onChange={(e) => setConfirmPassword(e.target.value)}
97
+                        />
98
+                    </Form.Item>
99
+                </div>
100
+                <Button
101
+                    type="primary"
102
+                    className="mt-4 w-fit mx-auto"
103
+                    onClick={() => console.log('Change password confirmed')}
104
+                >
105
+                    <span className='px-10 font-bold'>Save</span>
106
+                </Button>
107
+            </div>
108
+
109
+            {/* 5. Delete Account */}
110
+            <div>
111
+                <div className="flex items-center gap-2 mb-2">
112
+                    <DeleteOutlined className="text-xl" style={{ color: "red" }} />
113
+                    <p className="font-bold text-red-600 py-1 w-fit">
114
+                        Delete Account
115
+                    </p>
116
+                </div>
117
+                <Text type="danger">
118
+                    Your account and data will be permanently deleted.
119
+                </Text>
120
+                <div className="mt-4 flex flex-col">
121
+                    <Button
122
+                        danger
123
+                        className='w-fit mx-auto'
124
+                        type="primary"
125
+                        onClick={() => console.log('Account deletion confirmed')}
126
+                    >
127
+                        <span className='px-10 font-bold'>Delete</span>
128
+                    </Button>
129
+                </div>
130
+            </div>
131
+        </Form>
132
+    )
133
+}
134
+
135
+export default ProfileSettings

+ 67
- 0
app/user/wallet/page.tsx Bestand weergeven

@@ -0,0 +1,67 @@
1
+'use client';
2
+
3
+import React, { useState } from 'react';
4
+import PageTitle from '@/components/ui/PageTitle';
5
+import { Input, Radio, Typography } from 'antd';
6
+import Link from 'next/link';
7
+
8
+const { Text } = Typography;
9
+
10
+const Wallet: React.FC = () => {
11
+    const [amount, setAmount] = useState('');
12
+    const [autoRenew, setAutoRenew] = useState(false);
13
+
14
+    return (
15
+        <main className="flex flex-col bg-white min-h-screen">
16
+            <PageTitle title="WALLET" />
17
+
18
+            <div className="flex flex-col gap-y-6 mt-6 max-w-md">
19
+                <div className='px-3'>
20
+                    {/* 1. Top Section: Title + History */}
21
+                    <div className="flex justify-between items-center mb-2">
22
+                        <p className="font-bold">Current Balance</p>
23
+                        <Link href="/transaction-history" className="text-blue-500 underline text-sm">
24
+                            Transaction History
25
+                        </Link>
26
+                    </div>
27
+
28
+                    {/* 2. RM Input */}
29
+                    <div className='flex flex-col'>
30
+                        <Input
31
+                            className="border border-blue-500 bg-blue-50 mb-3"
32
+                            placeholder="Enter amount in RM"
33
+                            type='number'
34
+                            value={amount}
35
+                            onChange={(e) => setAmount(e.target.value)}
36
+                            prefix="RM"
37
+                            size="large"
38
+                        />
39
+                        <button className='w-fit rounded-lg text-xs px-4 py-2 bg-[#369cc1] hover:bg-[#436d86] text-white shadow border-0 mb-3 ms-auto'>
40
+                            <span className='font-bold'>Top Up Wallet</span>
41
+                        </button>
42
+                    </div>
43
+
44
+                    <div className="flex items-start gap-3 px-3">
45
+                        <Radio
46
+                            checked={autoRenew}
47
+                            onChange={(e) => setAutoRenew(e.target.checked)}
48
+                        />
49
+                        <div>
50
+                            <Text strong>Auto-Renew Subscription</Text>
51
+                            <p className="text-xs text-gray-500">
52
+                                Enable monthly or yearly auto payment using wallet.
53
+                            </p>
54
+                        </div>
55
+                    </div>
56
+                </div>
57
+
58
+                {/* 4. Current Plan */}
59
+                <div className='border-y-2 p-3 border-blue-700'>
60
+                    <p><span className='font-bold'>Current Plan:</span> <span >Premium (RM25/Month)</span> </p>
61
+                </div>
62
+            </div>
63
+        </main>
64
+    );
65
+};
66
+
67
+export default Wallet;

+ 16
- 0
components/general/QueryProvider.tsx Bestand weergeven

@@ -0,0 +1,16 @@
1
+'use client';
2
+
3
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+import { ReactNode, useState } from 'react';
5
+
6
+const QueryProvider = ({ children }: { children: ReactNode }) => {
7
+  const [queryClient] = useState(() => new QueryClient());
8
+
9
+  return (
10
+    <QueryClientProvider client={queryClient}>
11
+      {children}
12
+    </QueryClientProvider>
13
+  );
14
+};
15
+
16
+export default QueryProvider;

+ 39
- 0
components/layout/Header.tsx Bestand weergeven

@@ -0,0 +1,39 @@
1
+'use client'
2
+
3
+import { useState } from 'react'
4
+import { MenuOutlined } from '@ant-design/icons'
5
+import { Button, Space } from 'antd'
6
+import Sidebar from '@/components/layout/Sidebar'
7
+import BellNotification from '@/components/ui/BellNotification'
8
+
9
+const Header = () => {
10
+  const [showSideBar, setShowSideBar] = useState(false)
11
+
12
+  return (
13
+    <header className="flex items-center px-4 py-2 relative z-40 bg-white shadow-sm">
14
+      {/* Sidebar Trigger */}
15
+      <Button
16
+        type="text"
17
+        icon={<MenuOutlined />}
18
+        onClick={() => setShowSideBar(true)}
19
+        className="text-lg text-gray-700"
20
+      />
21
+
22
+      {/* Sidebar Component */}
23
+      <Sidebar open={showSideBar} setOpen={setShowSideBar} />
24
+
25
+      {/* Logo + Brand */}
26
+      <div className="flex items-center ms-3">
27
+        <img src="/ruccan_logo.png" alt="Ruccan Logo" className="h-8 w-auto" />
28
+        <span className="text-black font-bold text-lg ms-3">Ruccan.com</span>
29
+      </div>
30
+
31
+      {/* Notification Bell */}
32
+      <div className="ms-auto">
33
+        <BellNotification />
34
+      </div>
35
+    </header>
36
+  )
37
+}
38
+
39
+export default Header

+ 164
- 0
components/layout/Sidebar.tsx Bestand weergeven

@@ -0,0 +1,164 @@
1
+'use client'
2
+
3
+import { Drawer, Menu, Divider, Avatar, Typography } from 'antd'
4
+import {
5
+  HomeOutlined,
6
+  MessageOutlined,
7
+  UserOutlined,
8
+  BookOutlined,
9
+  ApiOutlined,
10
+  WalletOutlined,
11
+  SettingOutlined,
12
+  QuestionCircleOutlined,
13
+  LogoutOutlined,
14
+  LeftOutlined
15
+} from '@ant-design/icons'
16
+import { useRouter } from 'next/navigation'
17
+import type { SidebarProps } from '@/types/sidebar'
18
+
19
+const { Text } = Typography
20
+
21
+const Sidebar = ({ open = false, setOpen = () => { } }: SidebarProps) => {
22
+  const router = useRouter()
23
+
24
+  const handleClick = (e: { key: string }) => {
25
+    router.push(e.key)
26
+    setOpen(false)
27
+  }
28
+
29
+  return (
30
+    <Drawer
31
+      placement="left"
32
+      closable={false}
33
+      onClose={() => setOpen(false)}
34
+      open={open}
35
+      width={250}
36
+      mask={false}
37
+      styles={{
38
+        body: {
39
+          padding: "0 10px",
40
+        },
41
+      }}
42
+    >
43
+      {/* Custom Close Button */}
44
+      <button
45
+        className="p-2 bg-white hover:bg-gray-100 rounded shadow text-xs cursor-pointer"
46
+        onClick={() => {
47
+          setOpen(false)
48
+        }}
49
+        style={{
50
+          position: 'absolute',
51
+          top: 30,
52
+          right: -20,
53
+        }}
54
+      >
55
+        <LeftOutlined />
56
+      </button>
57
+
58
+      {/* Top: Avatar/Profile */}
59
+      <div className="flex items-center gap-3 px-4 py-4 border-b border-gray-200">
60
+        <Avatar size="large" icon={<UserOutlined />} />
61
+        <div>
62
+          <div className="text-xs text-gray-500">PRODUCT MANAGER</div>
63
+          <Text strong>Amirul Baharuddin</Text>
64
+        </div>
65
+      </div>
66
+
67
+      {/* Main Menu */}
68
+      <div className='border-b border-gray-200'>
69
+        <p className='px-7 pt-3 text-gray-500 text-xs text-bold'>MAIN</p>
70
+        <Menu
71
+          mode="inline"
72
+          onClick={handleClick}
73
+          items={[
74
+            {
75
+              key: '/dashboard',
76
+              icon: <HomeOutlined />,
77
+              label: 'Dashboard',
78
+            },
79
+            {
80
+              key: '/chat',
81
+              icon: <MessageOutlined />,
82
+              label: 'Ruccan Chat',
83
+            },
84
+            {
85
+              key: '/persona',
86
+              icon: <UserOutlined />,
87
+              label: 'Persona',
88
+            },
89
+            {
90
+              key: '/knowledge',
91
+              icon: <BookOutlined />,
92
+              label: 'Knowledge',
93
+            },
94
+            {
95
+              key: '/integration',
96
+              icon: <ApiOutlined />,
97
+              label: 'Integration',
98
+            },
99
+            {
100
+              key: '/wallet',
101
+              icon: <WalletOutlined />,
102
+              label: 'Wallet',
103
+            },
104
+          ]}
105
+        />
106
+      </div>
107
+
108
+      {/* Settings & Others */}
109
+      <div>
110
+        <p className='px-7 pt-3 text-gray-500 text-xs text-bold'>SETTINGS</p>
111
+        <Menu
112
+          mode="inline"
113
+          onClick={handleClick}
114
+          items={[
115
+            {
116
+              key: 'settings-parent',
117
+              icon: <SettingOutlined />,
118
+              label: 'Settings',
119
+              children: [
120
+                {
121
+                  key: '/settings/general',
122
+                  icon: <SettingOutlined />, // General settings still makes sense
123
+                  label: 'General',
124
+                },
125
+                {
126
+                  key: '/settings/profile',
127
+                  icon: <UserOutlined />, // Profile = user-related
128
+                  label: 'Profile',
129
+                },
130
+                {
131
+                  key: '/settings/chat-preference',
132
+                  icon: <MessageOutlined />, // Chat preference = chat/message
133
+                  label: 'Chat Preference',
134
+                }
135
+              ]
136
+            }
137
+          ]}
138
+        />
139
+      </div>
140
+      <div>
141
+        <Menu
142
+          mode="inline"
143
+          onClick={handleClick}
144
+          style={{ position: "absolute", bottom: 0, left: 0 }}
145
+          items={[
146
+
147
+            {
148
+              key: '/help',
149
+              icon: <QuestionCircleOutlined />,
150
+              label: 'Help',
151
+            },
152
+            {
153
+              key: '/logout',
154
+              icon: <LogoutOutlined style={{ color: "red" }} />,
155
+              label: <p className='text-red-400 font-bold'>Logout</p>,
156
+            },
157
+          ]}
158
+        />
159
+      </div>
160
+    </Drawer>
161
+  )
162
+}
163
+
164
+export default Sidebar

+ 72
- 0
components/ui/BellNotification.tsx Bestand weergeven

@@ -0,0 +1,72 @@
1
+'use client'
2
+
3
+import {
4
+  BellOutlined,
5
+} from '@ant-design/icons'
6
+import { Badge, Dropdown, Space, List, Typography } from 'antd'
7
+import type { MenuProps } from 'antd'
8
+
9
+const { Text } = Typography
10
+
11
+// Example Notification Data
12
+const notifications = [
13
+  {
14
+    id: '1',
15
+    title: 'New Message',
16
+    description: 'You have received a new message from John.',
17
+    time: '2 min ago',
18
+  },
19
+  {
20
+    id: '2',
21
+    title: 'Server Update',
22
+    description: 'Server will restart at 12:00 AM.',
23
+    time: '10 min ago',
24
+  },
25
+  {
26
+    id: '3',
27
+    title: 'Payment Received',
28
+    description: 'You have received a payment of $120.',
29
+    time: '1 hour ago',
30
+  },
31
+]
32
+
33
+const NotificationList = () => (
34
+  <div className="w-80 max-h-96 overflow-auto bg-white rounded p-2 border border-gray-200 absolute" style={{right:-16}}>
35
+    <List
36
+      itemLayout="horizontal"
37
+      dataSource={notifications}
38
+      renderItem={(item) => (
39
+        <List.Item className="hover:bg-gray-50 px-3 py-2 cursor-pointer transition-all">
40
+          <List.Item.Meta
41
+            title={<Text strong>{item.title}</Text>}
42
+            description={
43
+              <div className="flex justify-between text-xs text-gray-500">
44
+                <span>{item.description}</span>
45
+                <span className="whitespace-nowrap">{item.time}</span>
46
+              </div>
47
+            }
48
+          />
49
+        </List.Item>
50
+      )}
51
+    />
52
+  </div>
53
+)
54
+
55
+const BellNotification = () => {
56
+  return (
57
+    <Dropdown
58
+      popupRender={() => <NotificationList />}
59
+      placement="bottomRight"
60
+      trigger={['click']}
61
+      arrow
62
+    >
63
+      <Space className="cursor-pointer relative">
64
+        <Badge count={notifications.length} size="small" offset={[-2, 2]}>
65
+          <BellOutlined style={{ fontSize: 20, color: 'black' }} />
66
+        </Badge>
67
+      </Space>
68
+    </Dropdown>
69
+  )
70
+}
71
+
72
+export default BellNotification

+ 47
- 0
components/ui/InputList.tsx Bestand weergeven

@@ -0,0 +1,47 @@
1
+import React from 'react';
2
+import { Input, Form, Button, Space } from 'antd';
3
+import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
4
+
5
+const InputList: React.FC = () => {
6
+  return (
7
+    <div className='mt-2'>
8
+      <Form.List name="weblinks">
9
+        {(fields, { add, remove }) => (
10
+          <Space direction="vertical" style={{ width: '100%' }}>
11
+            {fields.map(({ key, name, ...restField }) => (
12
+              <Space key={key} align="baseline" style={{ width: '100%' }}>
13
+                <Form.Item
14
+                  {...restField}
15
+                  name={[name, 'first']}
16
+                  rules={[{ required: true, message: 'Missing link' }]}
17
+                  style={{ flex: 1 }}
18
+                >
19
+                  <Input placeholder="https://example.com/faq" />
20
+                </Form.Item>
21
+                <DeleteOutlined
22
+                  className='!text-red-500'
23
+                  onClick={() => remove(name)}
24
+                />
25
+              </Space>
26
+            ))}
27
+
28
+            {fields.length < 5 && (
29
+              <Form.Item>
30
+                <Button
31
+                  type="dashed"
32
+                  onClick={() => add()}
33
+                  block
34
+                  icon={<PlusOutlined />}
35
+                >
36
+                  Add field
37
+                </Button>
38
+              </Form.Item>
39
+            )}
40
+          </Space>
41
+        )}
42
+      </Form.List>
43
+    </div>
44
+  );
45
+};
46
+
47
+export default InputList;

+ 35
- 0
components/ui/LoadingMeter.tsx Bestand weergeven

@@ -0,0 +1,35 @@
1
+import React from 'react';
2
+
3
+type LoadingMeterProps = {
4
+    title: string,
5
+    progress: number,
6
+    total: number
7
+}
8
+
9
+const LoadingMeter: React.FC<LoadingMeterProps> = ({ title = "Title", progress = 0, total = 0 }) => {
10
+
11
+
12
+    const barProgress = (progress * 100) / total;
13
+
14
+    if(progress > total) return <p className='text-red-500'>Progress is more than total</p>
15
+
16
+    return (
17
+        <div className="flex flex-col gap-2 w-full max-w-md mb-2">
18
+            {/* Header Row */}
19
+            <div className="flex flex-row justify-between text-sm font-medium text-gray-700">
20
+                <span>{`${title}: `}</span>
21
+                <span>{`${progress}/${total}`}</span>
22
+            </div>
23
+
24
+            {/* Progress Bar */}
25
+            <div className="w-full h-3 bg-gray-200 rounded-full border overflow-hidden">
26
+                <div
27
+                    className="h-full bg-[#b64fb1] rounded-full transition-all duration-500"
28
+                    style={{ width: `${barProgress}%` }}
29
+                ></div>
30
+            </div>
31
+        </div>
32
+    );
33
+};
34
+
35
+export default LoadingMeter;

+ 20
- 0
components/ui/PageTitle.tsx Bestand weergeven

@@ -0,0 +1,20 @@
1
+import React from 'react'
2
+import { Typography } from 'antd';
3
+const { Title } = Typography;
4
+
5
+type PageTitleProps = {
6
+    title: string
7
+}
8
+
9
+const PageTitle: React.FC<PageTitleProps> = ({ title }) => {
10
+    return (
11
+        <div className='bg-[#480066] flex justify-center py-3'>
12
+            <Title level={4} style={{margin:0, color:"white"}}>
13
+                {title}
14
+            </Title>
15
+
16
+        </div>
17
+    )
18
+}
19
+
20
+export default PageTitle

+ 24
- 0
components/ui/SectionTitle.tsx Bestand weergeven

@@ -0,0 +1,24 @@
1
+import React from 'react'
2
+import { Typography, Divider } from 'antd';
3
+const { Title } = Typography;
4
+
5
+type SectionTitleProps = {
6
+    title: string
7
+}
8
+
9
+const SectionTitle: React.FC<SectionTitleProps> = ({ title }) => {
10
+    return (
11
+        <div className='relative bg-black'>
12
+            <Divider />
13
+            <Title
14
+                level={5}
15
+                className="absolute left-1/2 -translate-x-1/2 -top-3 bg-white px-2 text-white"
16
+                style={{ margin: 0 }}
17
+            >
18
+                {title}
19
+            </Title>
20
+        </div>
21
+    )
22
+}
23
+
24
+export default SectionTitle

+ 1435
- 6
package-lock.json
Diff onderdrukt omdat het te groot bestand
Bestand weergeven


+ 13
- 6
package.json Bestand weergeven

@@ -9,19 +9,26 @@
9 9
     "lint": "next lint"
10 10
   },
11 11
   "dependencies": {
12
+    "@ant-design/icons": "^6.0.0",
13
+    "@tanstack/react-query": "^5.81.5",
14
+    "antd": "^5.26.3",
15
+    "lucide-react": "^0.525.0",
16
+    "next": "15.3.4",
17
+    "next-auth": "^4.24.11",
12 18
     "react": "^19.0.0",
13
-    "react-dom": "^19.0.0",
14
-    "next": "15.3.4"
19
+    "react-dom": "^19.0.0"
15 20
   },
16 21
   "devDependencies": {
17
-    "typescript": "^5",
22
+    "@eslint/eslintrc": "^3",
23
+    "@tailwindcss/postcss": "^4",
18 24
     "@types/node": "^20",
19 25
     "@types/react": "^19",
20 26
     "@types/react-dom": "^19",
21
-    "@tailwindcss/postcss": "^4",
22
-    "tailwindcss": "^4",
27
+    "autoprefixer": "^10.4.21",
23 28
     "eslint": "^9",
24 29
     "eslint-config-next": "15.3.4",
25
-    "@eslint/eslintrc": "^3"
30
+    "postcss": "^8.5.6",
31
+    "tailwindcss": "^4.1.11",
32
+    "typescript": "^5"
26 33
   }
27 34
 }

BIN
public/flask.png Bestand weergeven


BIN
public/ruccan_logo.png Bestand weergeven


+ 6
- 0
types/bellnotification.ts Bestand weergeven

@@ -0,0 +1,6 @@
1
+import { Dispatch, SetStateAction } from 'react'
2
+
3
+export interface BellNotificationProps {
4
+    open: boolean,
5
+    setOpen: Dispatch<SetStateAction<boolean>>
6
+}

+ 17
- 0
types/chat.ts Bestand weergeven

@@ -0,0 +1,17 @@
1
+import { Persona } from "./persona";
2
+
3
+export interface ChatItem {
4
+    self:boolean,
5
+    text:string,
6
+    time:string,
7
+    date:string,
8
+    read:boolean,
9
+}
10
+
11
+export interface Chat {
12
+    id: number;
13
+    name: string;
14
+    persona: Persona
15
+    chatList: ChatItem[]
16
+};
17
+

+ 6
- 0
types/payment.ts Bestand weergeven

@@ -0,0 +1,6 @@
1
+export interface PaymentType {
2
+    id: number,
3
+    type: string,
4
+    description: string,
5
+    icon_id: string
6
+}

+ 12
- 0
types/persona.ts Bestand weergeven

@@ -0,0 +1,12 @@
1
+export interface PersonaStyle {
2
+    id: number, 
3
+    name: string
4
+}
5
+
6
+export interface Persona {
7
+    id: number;
8
+    name: string;
9
+    styles: PersonaStyle[];
10
+    role: string;
11
+    active: boolean;
12
+}

+ 6
- 0
types/sidebar.ts Bestand weergeven

@@ -0,0 +1,6 @@
1
+import { Dispatch, SetStateAction } from 'react'
2
+
3
+export interface SidebarProps {
4
+    open: boolean,
5
+    setOpen: Dispatch<SetStateAction<boolean>>
6
+}

Laden…
Annuleren
Opslaan