Spaces:
Sleeping
Sleeping
delete: delete trash logo on /dosen/course_id page
Browse files- resources/css/app.css +47 -27
- resources/js/components/auth/register-role-form.tsx +9 -1
- resources/js/components/student/question-composer.tsx +6 -1
- resources/js/components/student/student-shell.tsx +13 -2
- resources/js/components/ui/loading-indicator.tsx +49 -0
- resources/js/pages/auth/login.tsx +9 -1
- resources/js/pages/dosen-detail.tsx +56 -28
- resources/js/pages/dosen.tsx +30 -8
- resources/js/pages/mahasiswa-chat.tsx +6 -2
resources/css/app.css
CHANGED
|
@@ -320,7 +320,54 @@ @layer base {
|
|
| 320 |
}
|
| 321 |
}
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
@layer components {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
.welcome-shell {
|
| 325 |
@apply relative flex min-h-svh items-center justify-center px-4 py-8 sm:px-6;
|
| 326 |
background-color: var(--welcome-shell-bg);
|
|
@@ -1884,29 +1931,6 @@ @layer components {
|
|
| 1884 |
color: var(--lecturer-primary);
|
| 1885 |
}
|
| 1886 |
|
| 1887 |
-
.lecturer-document-actions {
|
| 1888 |
-
@apply ml-auto flex shrink-0 items-center gap-3;
|
| 1889 |
-
}
|
| 1890 |
-
|
| 1891 |
-
.lecturer-document-delete {
|
| 1892 |
-
@apply inline-flex size-8 items-center justify-center rounded-[7px] transition-colors outline-none focus-visible:ring-2;
|
| 1893 |
-
color: var(--lecturer-muted);
|
| 1894 |
-
--tw-ring-color: color-mix(
|
| 1895 |
-
in oklab,
|
| 1896 |
-
var(--lecturer-primary) 45%,
|
| 1897 |
-
transparent
|
| 1898 |
-
);
|
| 1899 |
-
}
|
| 1900 |
-
|
| 1901 |
-
.lecturer-document-delete:hover {
|
| 1902 |
-
background-color: color-mix(
|
| 1903 |
-
in oklab,
|
| 1904 |
-
var(--lecturer-danger) 10%,
|
| 1905 |
-
var(--lecturer-surface)
|
| 1906 |
-
);
|
| 1907 |
-
color: var(--lecturer-danger);
|
| 1908 |
-
}
|
| 1909 |
-
|
| 1910 |
.lecturer-detail-empty {
|
| 1911 |
@apply rounded-[10px] border px-5 py-8 text-center text-sm;
|
| 1912 |
background-color: var(--lecturer-surface);
|
|
@@ -1971,9 +1995,5 @@ @layer components {
|
|
| 1971 |
.lecturer-document-content {
|
| 1972 |
@apply flex-col;
|
| 1973 |
}
|
| 1974 |
-
|
| 1975 |
-
.lecturer-document-actions {
|
| 1976 |
-
@apply ml-0 w-full justify-between;
|
| 1977 |
-
}
|
| 1978 |
}
|
| 1979 |
}
|
|
|
|
| 320 |
}
|
| 321 |
}
|
| 322 |
|
| 323 |
+
@keyframes app-loading-dot {
|
| 324 |
+
0%,
|
| 325 |
+
80%,
|
| 326 |
+
100% {
|
| 327 |
+
opacity: 0.24;
|
| 328 |
+
transform: translateY(0);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
40% {
|
| 332 |
+
opacity: 1;
|
| 333 |
+
transform: translateY(-0.12rem);
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
@media (prefers-reduced-motion: reduce) {
|
| 338 |
+
.app-loading-dot {
|
| 339 |
+
animation: none !important;
|
| 340 |
+
opacity: 1;
|
| 341 |
+
transform: none;
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
@layer components {
|
| 346 |
+
.app-loading-indicator {
|
| 347 |
+
@apply inline-flex items-center justify-center gap-2 whitespace-nowrap;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.app-loading-spinner {
|
| 351 |
+
@apply shrink-0;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.app-loading-dots {
|
| 355 |
+
@apply inline-flex min-w-[1.05em] items-center justify-start;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.app-loading-dot {
|
| 359 |
+
animation: app-loading-dot 1.2s infinite ease-in-out both;
|
| 360 |
+
line-height: 1;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.app-loading-dot:nth-child(2) {
|
| 364 |
+
animation-delay: 0.16s;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.app-loading-dot:nth-child(3) {
|
| 368 |
+
animation-delay: 0.32s;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
.welcome-shell {
|
| 372 |
@apply relative flex min-h-svh items-center justify-center px-4 py-8 sm:px-6;
|
| 373 |
background-color: var(--welcome-shell-bg);
|
|
|
|
| 1931 |
color: var(--lecturer-primary);
|
| 1932 |
}
|
| 1933 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1934 |
.lecturer-detail-empty {
|
| 1935 |
@apply rounded-[10px] border px-5 py-8 text-center text-sm;
|
| 1936 |
background-color: var(--lecturer-surface);
|
|
|
|
| 1995 |
.lecturer-document-content {
|
| 1996 |
@apply flex-col;
|
| 1997 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1998 |
}
|
| 1999 |
}
|
resources/js/components/auth/register-role-form.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
|
|
| 7 |
import { Card, CardContent } from '@/components/ui/card';
|
| 8 |
import { Input } from '@/components/ui/input';
|
| 9 |
import { Label } from '@/components/ui/label';
|
|
|
|
| 10 |
import { authApi, getAuthErrorMessage } from '@/lib/auth';
|
| 11 |
import { register } from '@/routes';
|
| 12 |
|
|
@@ -357,7 +358,14 @@ export default function RegisterRoleForm({ role }: RegisterRoleFormProps) {
|
|
| 357 |
disabled={isProcessing}
|
| 358 |
data-test="register-role-button"
|
| 359 |
>
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
</Button>
|
| 362 |
</form>
|
| 363 |
|
|
|
|
| 7 |
import { Card, CardContent } from '@/components/ui/card';
|
| 8 |
import { Input } from '@/components/ui/input';
|
| 9 |
import { Label } from '@/components/ui/label';
|
| 10 |
+
import { LoadingIndicator } from '@/components/ui/loading-indicator';
|
| 11 |
import { authApi, getAuthErrorMessage } from '@/lib/auth';
|
| 12 |
import { register } from '@/routes';
|
| 13 |
|
|
|
|
| 358 |
disabled={isProcessing}
|
| 359 |
data-test="register-role-button"
|
| 360 |
>
|
| 361 |
+
{isProcessing ? (
|
| 362 |
+
<LoadingIndicator
|
| 363 |
+
label="Mendaftar"
|
| 364 |
+
spinnerClassName="size-3.5"
|
| 365 |
+
/>
|
| 366 |
+
) : (
|
| 367 |
+
'Register'
|
| 368 |
+
)}
|
| 369 |
</Button>
|
| 370 |
</form>
|
| 371 |
|
resources/js/components/student/question-composer.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
| 10 |
SelectTrigger,
|
| 11 |
SelectValue,
|
| 12 |
} from '@/components/ui/select';
|
|
|
|
| 13 |
import type { CourseResponse } from '@/lib/rag';
|
| 14 |
import { cn } from '@/lib/utils';
|
| 15 |
|
|
@@ -118,7 +119,11 @@ export function QuestionComposer({
|
|
| 118 |
size="icon"
|
| 119 |
type="submit"
|
| 120 |
>
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
</Button>
|
| 123 |
</form>
|
| 124 |
</CardContent>
|
|
|
|
| 10 |
SelectTrigger,
|
| 11 |
SelectValue,
|
| 12 |
} from '@/components/ui/select';
|
| 13 |
+
import { Spinner } from '@/components/ui/spinner';
|
| 14 |
import type { CourseResponse } from '@/lib/rag';
|
| 15 |
import { cn } from '@/lib/utils';
|
| 16 |
|
|
|
|
| 119 |
size="icon"
|
| 120 |
type="submit"
|
| 121 |
>
|
| 122 |
+
{isProcessing ? (
|
| 123 |
+
<Spinner className="size-5" />
|
| 124 |
+
) : (
|
| 125 |
+
<Send className="size-5" />
|
| 126 |
+
)}
|
| 127 |
</Button>
|
| 128 |
</form>
|
| 129 |
</CardContent>
|
resources/js/components/student/student-shell.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
| 20 |
DialogHeader,
|
| 21 |
DialogTitle,
|
| 22 |
} from '@/components/ui/dialog';
|
|
|
|
| 23 |
import {
|
| 24 |
Sidebar,
|
| 25 |
SidebarContent,
|
|
@@ -250,7 +251,10 @@ function StudentSidebar({
|
|
| 250 |
<Clock3 className="student-history-menu-icon size-4" />
|
| 251 |
<span className="student-history-copy">
|
| 252 |
<span className="student-history-title">
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
| 254 |
</span>
|
| 255 |
</span>
|
| 256 |
</SidebarMenuButton>
|
|
@@ -421,7 +425,14 @@ function StudentSidebar({
|
|
| 421 |
}}
|
| 422 |
type="button"
|
| 423 |
>
|
| 424 |
-
{isDeletingSession ?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
</Button>
|
| 426 |
</DialogFooter>
|
| 427 |
</DialogContent>
|
|
|
|
| 20 |
DialogHeader,
|
| 21 |
DialogTitle,
|
| 22 |
} from '@/components/ui/dialog';
|
| 23 |
+
import { LoadingIndicator } from '@/components/ui/loading-indicator';
|
| 24 |
import {
|
| 25 |
Sidebar,
|
| 26 |
SidebarContent,
|
|
|
|
| 251 |
<Clock3 className="student-history-menu-icon size-4" />
|
| 252 |
<span className="student-history-copy">
|
| 253 |
<span className="student-history-title">
|
| 254 |
+
<LoadingIndicator
|
| 255 |
+
label="Memuat riwayat"
|
| 256 |
+
spinnerClassName="size-3"
|
| 257 |
+
/>
|
| 258 |
</span>
|
| 259 |
</span>
|
| 260 |
</SidebarMenuButton>
|
|
|
|
| 425 |
}}
|
| 426 |
type="button"
|
| 427 |
>
|
| 428 |
+
{isDeletingSession ? (
|
| 429 |
+
<LoadingIndicator
|
| 430 |
+
label="Menghapus"
|
| 431 |
+
spinnerClassName="size-3.5"
|
| 432 |
+
/>
|
| 433 |
+
) : (
|
| 434 |
+
'Hapus'
|
| 435 |
+
)}
|
| 436 |
</Button>
|
| 437 |
</DialogFooter>
|
| 438 |
</DialogContent>
|
resources/js/components/ui/loading-indicator.tsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ComponentProps } from 'react';
|
| 2 |
+
|
| 3 |
+
import { Spinner } from '@/components/ui/spinner';
|
| 4 |
+
import { cn } from '@/lib/utils';
|
| 5 |
+
|
| 6 |
+
type LoadingIndicatorProps = ComponentProps<'span'> & {
|
| 7 |
+
label?: string;
|
| 8 |
+
showSpinner?: boolean;
|
| 9 |
+
spinnerClassName?: string;
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
function LoadingDots({ className }: { className?: string }) {
|
| 13 |
+
return (
|
| 14 |
+
<span aria-hidden="true" className={cn('app-loading-dots', className)}>
|
| 15 |
+
<span className="app-loading-dot">.</span>
|
| 16 |
+
<span className="app-loading-dot">.</span>
|
| 17 |
+
<span className="app-loading-dot">.</span>
|
| 18 |
+
</span>
|
| 19 |
+
);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function LoadingIndicator({
|
| 23 |
+
className,
|
| 24 |
+
label = 'Memuat',
|
| 25 |
+
showSpinner = true,
|
| 26 |
+
spinnerClassName,
|
| 27 |
+
...props
|
| 28 |
+
}: LoadingIndicatorProps) {
|
| 29 |
+
return (
|
| 30 |
+
<span
|
| 31 |
+
aria-live="polite"
|
| 32 |
+
className={cn('app-loading-indicator', className)}
|
| 33 |
+
role="status"
|
| 34 |
+
{...props}
|
| 35 |
+
>
|
| 36 |
+
{showSpinner && (
|
| 37 |
+
<Spinner
|
| 38 |
+
aria-hidden="true"
|
| 39 |
+
className={cn('app-loading-spinner', spinnerClassName)}
|
| 40 |
+
role="presentation"
|
| 41 |
+
/>
|
| 42 |
+
)}
|
| 43 |
+
{label && <span>{label}</span>}
|
| 44 |
+
<LoadingDots />
|
| 45 |
+
</span>
|
| 46 |
+
);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export { LoadingDots, LoadingIndicator };
|
resources/js/pages/auth/login.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|
| 7 |
import { Checkbox } from '@/components/ui/checkbox';
|
| 8 |
import { Input } from '@/components/ui/input';
|
| 9 |
import { Label } from '@/components/ui/label';
|
|
|
|
| 10 |
import { authApi, getAuthErrorMessage, saveAuthSession } from '@/lib/auth';
|
| 11 |
import { dashboard, dosen, home, mahasiswa, register } from '@/routes';
|
| 12 |
|
|
@@ -137,7 +138,14 @@ export default function Login({ canRegister = true, status }: LoginProps) {
|
|
| 137 |
disabled={isProcessing}
|
| 138 |
data-test="login-button"
|
| 139 |
>
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
</Button>
|
| 142 |
</form>
|
| 143 |
|
|
|
|
| 7 |
import { Checkbox } from '@/components/ui/checkbox';
|
| 8 |
import { Input } from '@/components/ui/input';
|
| 9 |
import { Label } from '@/components/ui/label';
|
| 10 |
+
import { LoadingIndicator } from '@/components/ui/loading-indicator';
|
| 11 |
import { authApi, getAuthErrorMessage, saveAuthSession } from '@/lib/auth';
|
| 12 |
import { dashboard, dosen, home, mahasiswa, register } from '@/routes';
|
| 13 |
|
|
|
|
| 138 |
disabled={isProcessing}
|
| 139 |
data-test="login-button"
|
| 140 |
>
|
| 141 |
+
{isProcessing ? (
|
| 142 |
+
<LoadingIndicator
|
| 143 |
+
label="Masuk"
|
| 144 |
+
spinnerClassName="size-3.5"
|
| 145 |
+
/>
|
| 146 |
+
) : (
|
| 147 |
+
'Sign In'
|
| 148 |
+
)}
|
| 149 |
</Button>
|
| 150 |
</form>
|
| 151 |
|
resources/js/pages/dosen-detail.tsx
CHANGED
|
@@ -25,6 +25,10 @@ import {
|
|
| 25 |
} from '@/components/ui/dialog';
|
| 26 |
import { Input } from '@/components/ui/input';
|
| 27 |
import { Label } from '@/components/ui/label';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
import { getStoredAuthToken } from '@/lib/auth';
|
| 29 |
import type {
|
| 30 |
CourseResponse,
|
|
@@ -142,19 +146,16 @@ function DocumentCard({ document }: { document: DocumentResponse }) {
|
|
| 142 |
data-status={statusTone}
|
| 143 |
>
|
| 144 |
<DocumentStatusIcon status={document.status} />
|
| 145 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
</p>
|
| 147 |
</div>
|
| 148 |
-
|
| 149 |
-
<div className="lecturer-document-actions">
|
| 150 |
-
<button
|
| 151 |
-
aria-label={`Hapus ${document.filename}`}
|
| 152 |
-
className="lecturer-document-delete"
|
| 153 |
-
type="button"
|
| 154 |
-
>
|
| 155 |
-
<Trash2 className="size-4" />
|
| 156 |
-
</button>
|
| 157 |
-
</div>
|
| 158 |
</CardContent>
|
| 159 |
</Card>
|
| 160 |
);
|
|
@@ -338,11 +339,6 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 338 |
const title =
|
| 339 |
course?.title ??
|
| 340 |
(isLoading ? 'Memuat mata kuliah...' : 'Detail mata kuliah');
|
| 341 |
-
const description =
|
| 342 |
-
course?.description ??
|
| 343 |
-
(isLoading
|
| 344 |
-
? 'Mengambil detail mata kuliah dari backend.'
|
| 345 |
-
: 'Tidak ada deskripsi mata kuliah.');
|
| 346 |
|
| 347 |
return (
|
| 348 |
<>
|
|
@@ -362,9 +358,26 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 362 |
|
| 363 |
<div className="lecturer-detail-heading">
|
| 364 |
<div className="lecturer-detail-title-row">
|
| 365 |
-
<h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
</div>
|
| 367 |
-
<p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
</div>
|
| 369 |
|
| 370 |
<div className="lecturer-detail-actions">
|
|
@@ -417,9 +430,14 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 417 |
)}
|
| 418 |
</span>
|
| 419 |
<span className="lecturer-upload-title">
|
| 420 |
-
{isUploading
|
| 421 |
-
|
| 422 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
</span>
|
| 424 |
<span className="lecturer-upload-description">
|
| 425 |
Drag & drop file PDF, PPT, atau TXT di sini.
|
|
@@ -440,7 +458,7 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 440 |
<div className="lecturer-document-list">
|
| 441 |
{isLoading && (
|
| 442 |
<p className="lecturer-detail-empty">
|
| 443 |
-
Memuat dokumen
|
| 444 |
</p>
|
| 445 |
)}
|
| 446 |
|
|
@@ -548,9 +566,14 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 548 |
disabled={isUpdatingCourse}
|
| 549 |
type="submit"
|
| 550 |
>
|
| 551 |
-
{isUpdatingCourse
|
| 552 |
-
|
| 553 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
</Button>
|
| 555 |
</DialogFooter>
|
| 556 |
</form>
|
|
@@ -604,9 +627,14 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 604 |
}}
|
| 605 |
type="button"
|
| 606 |
>
|
| 607 |
-
{isDeletingCourse
|
| 608 |
-
|
| 609 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 610 |
</Button>
|
| 611 |
</DialogFooter>
|
| 612 |
</div>
|
|
|
|
| 25 |
} from '@/components/ui/dialog';
|
| 26 |
import { Input } from '@/components/ui/input';
|
| 27 |
import { Label } from '@/components/ui/label';
|
| 28 |
+
import {
|
| 29 |
+
LoadingDots,
|
| 30 |
+
LoadingIndicator,
|
| 31 |
+
} from '@/components/ui/loading-indicator';
|
| 32 |
import { getStoredAuthToken } from '@/lib/auth';
|
| 33 |
import type {
|
| 34 |
CourseResponse,
|
|
|
|
| 146 |
data-status={statusTone}
|
| 147 |
>
|
| 148 |
<DocumentStatusIcon status={document.status} />
|
| 149 |
+
{document.status === 'processing' ? (
|
| 150 |
+
<>
|
| 151 |
+
<span>Sedang diproses</span>
|
| 152 |
+
<LoadingDots />
|
| 153 |
+
</>
|
| 154 |
+
) : (
|
| 155 |
+
getStatusLabel(document.status)
|
| 156 |
+
)}
|
| 157 |
</p>
|
| 158 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
</CardContent>
|
| 160 |
</Card>
|
| 161 |
);
|
|
|
|
| 339 |
const title =
|
| 340 |
course?.title ??
|
| 341 |
(isLoading ? 'Memuat mata kuliah...' : 'Detail mata kuliah');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
|
| 343 |
return (
|
| 344 |
<>
|
|
|
|
| 358 |
|
| 359 |
<div className="lecturer-detail-heading">
|
| 360 |
<div className="lecturer-detail-title-row">
|
| 361 |
+
<h1>
|
| 362 |
+
{course?.title ??
|
| 363 |
+
(isLoading ? (
|
| 364 |
+
<LoadingIndicator label="Memuat mata kuliah" />
|
| 365 |
+
) : (
|
| 366 |
+
'Detail mata kuliah'
|
| 367 |
+
))}
|
| 368 |
+
</h1>
|
| 369 |
</div>
|
| 370 |
+
<p>
|
| 371 |
+
{course?.description ??
|
| 372 |
+
(isLoading ? (
|
| 373 |
+
<LoadingIndicator
|
| 374 |
+
label="Mengambil detail mata kuliah"
|
| 375 |
+
showSpinner={false}
|
| 376 |
+
/>
|
| 377 |
+
) : (
|
| 378 |
+
'Tidak ada deskripsi mata kuliah.'
|
| 379 |
+
))}
|
| 380 |
+
</p>
|
| 381 |
</div>
|
| 382 |
|
| 383 |
<div className="lecturer-detail-actions">
|
|
|
|
| 430 |
)}
|
| 431 |
</span>
|
| 432 |
<span className="lecturer-upload-title">
|
| 433 |
+
{isUploading ? (
|
| 434 |
+
<LoadingIndicator
|
| 435 |
+
label="Mengupload dokumen"
|
| 436 |
+
showSpinner={false}
|
| 437 |
+
/>
|
| 438 |
+
) : (
|
| 439 |
+
'Upload Dokumen Baru'
|
| 440 |
+
)}
|
| 441 |
</span>
|
| 442 |
<span className="lecturer-upload-description">
|
| 443 |
Drag & drop file PDF, PPT, atau TXT di sini.
|
|
|
|
| 458 |
<div className="lecturer-document-list">
|
| 459 |
{isLoading && (
|
| 460 |
<p className="lecturer-detail-empty">
|
| 461 |
+
<LoadingIndicator label="Memuat dokumen" />
|
| 462 |
</p>
|
| 463 |
)}
|
| 464 |
|
|
|
|
| 566 |
disabled={isUpdatingCourse}
|
| 567 |
type="submit"
|
| 568 |
>
|
| 569 |
+
{isUpdatingCourse ? (
|
| 570 |
+
<LoadingIndicator
|
| 571 |
+
label="Menyimpan"
|
| 572 |
+
spinnerClassName="size-3.5"
|
| 573 |
+
/>
|
| 574 |
+
) : (
|
| 575 |
+
'Simpan'
|
| 576 |
+
)}
|
| 577 |
</Button>
|
| 578 |
</DialogFooter>
|
| 579 |
</form>
|
|
|
|
| 627 |
}}
|
| 628 |
type="button"
|
| 629 |
>
|
| 630 |
+
{isDeletingCourse ? (
|
| 631 |
+
<LoadingIndicator
|
| 632 |
+
label="Menghapus"
|
| 633 |
+
spinnerClassName="size-3.5"
|
| 634 |
+
/>
|
| 635 |
+
) : (
|
| 636 |
+
'Hapus'
|
| 637 |
+
)}
|
| 638 |
</Button>
|
| 639 |
</DialogFooter>
|
| 640 |
</div>
|
resources/js/pages/dosen.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { Head, router } from '@inertiajs/react';
|
|
| 2 |
import type { LucideIcon } from 'lucide-react';
|
| 3 |
import { AlertTriangle, Database, FileText, Plus, Search } from 'lucide-react';
|
| 4 |
import type { FormEvent } from 'react';
|
|
|
|
| 5 |
import { useCallback, useEffect, useMemo, useState } from 'react';
|
| 6 |
|
| 7 |
import AppearanceToggle from '@/components/appearance-toggle';
|
|
@@ -18,6 +19,10 @@ import {
|
|
| 18 |
} from '@/components/ui/dialog';
|
| 19 |
import { Input } from '@/components/ui/input';
|
| 20 |
import { Label } from '@/components/ui/label';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
import { getStoredAuthToken } from '@/lib/auth';
|
| 22 |
import type { CourseResponse, DocumentResponse } from '@/lib/rag';
|
| 23 |
import { getRagErrorMessage, ragApi } from '@/lib/rag';
|
|
@@ -34,7 +39,7 @@ type KnowledgeBaseRow = {
|
|
| 34 |
|
| 35 |
type Metric = {
|
| 36 |
label: string;
|
| 37 |
-
value:
|
| 38 |
description?: string;
|
| 39 |
icon: LucideIcon;
|
| 40 |
tone: 'primary' | 'danger';
|
|
@@ -252,21 +257,33 @@ export default function Dosen() {
|
|
| 252 |
icon: Database,
|
| 253 |
label: 'Total Knowledge Base',
|
| 254 |
tone: 'primary',
|
| 255 |
-
value: isLoading ?
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
},
|
| 257 |
{
|
| 258 |
icon: FileText,
|
| 259 |
label: 'Total Dokumen',
|
| 260 |
description: 'Chunked',
|
| 261 |
tone: 'primary',
|
| 262 |
-
value: isLoading ?
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
},
|
| 264 |
{
|
| 265 |
icon: AlertTriangle,
|
| 266 |
label: 'Processing Error',
|
| 267 |
description: 'dokumen',
|
| 268 |
tone: 'danger',
|
| 269 |
-
value: isLoading ?
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
},
|
| 271 |
];
|
| 272 |
}, [isLoading, knowledgeBases]);
|
|
@@ -360,7 +377,7 @@ export default function Dosen() {
|
|
| 360 |
className="lecturer-table-empty"
|
| 361 |
colSpan={4}
|
| 362 |
>
|
| 363 |
-
Memuat knowledge base
|
| 364 |
</td>
|
| 365 |
</tr>
|
| 366 |
)}
|
|
@@ -527,9 +544,14 @@ export default function Dosen() {
|
|
| 527 |
disabled={isCreatingCourse}
|
| 528 |
type="submit"
|
| 529 |
>
|
| 530 |
-
{isCreatingCourse
|
| 531 |
-
|
| 532 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
</Button>
|
| 534 |
</DialogFooter>
|
| 535 |
</form>
|
|
|
|
| 2 |
import type { LucideIcon } from 'lucide-react';
|
| 3 |
import { AlertTriangle, Database, FileText, Plus, Search } from 'lucide-react';
|
| 4 |
import type { FormEvent } from 'react';
|
| 5 |
+
import type { ReactNode } from 'react';
|
| 6 |
import { useCallback, useEffect, useMemo, useState } from 'react';
|
| 7 |
|
| 8 |
import AppearanceToggle from '@/components/appearance-toggle';
|
|
|
|
| 19 |
} from '@/components/ui/dialog';
|
| 20 |
import { Input } from '@/components/ui/input';
|
| 21 |
import { Label } from '@/components/ui/label';
|
| 22 |
+
import {
|
| 23 |
+
LoadingDots,
|
| 24 |
+
LoadingIndicator,
|
| 25 |
+
} from '@/components/ui/loading-indicator';
|
| 26 |
import { getStoredAuthToken } from '@/lib/auth';
|
| 27 |
import type { CourseResponse, DocumentResponse } from '@/lib/rag';
|
| 28 |
import { getRagErrorMessage, ragApi } from '@/lib/rag';
|
|
|
|
| 39 |
|
| 40 |
type Metric = {
|
| 41 |
label: string;
|
| 42 |
+
value: ReactNode;
|
| 43 |
description?: string;
|
| 44 |
icon: LucideIcon;
|
| 45 |
tone: 'primary' | 'danger';
|
|
|
|
| 257 |
icon: Database,
|
| 258 |
label: 'Total Knowledge Base',
|
| 259 |
tone: 'primary',
|
| 260 |
+
value: isLoading ? (
|
| 261 |
+
<LoadingDots className="lecturer-metric-loading" />
|
| 262 |
+
) : (
|
| 263 |
+
String(knowledgeBases.length)
|
| 264 |
+
),
|
| 265 |
},
|
| 266 |
{
|
| 267 |
icon: FileText,
|
| 268 |
label: 'Total Dokumen',
|
| 269 |
description: 'Chunked',
|
| 270 |
tone: 'primary',
|
| 271 |
+
value: isLoading ? (
|
| 272 |
+
<LoadingDots className="lecturer-metric-loading" />
|
| 273 |
+
) : (
|
| 274 |
+
String(documentCount)
|
| 275 |
+
),
|
| 276 |
},
|
| 277 |
{
|
| 278 |
icon: AlertTriangle,
|
| 279 |
label: 'Processing Error',
|
| 280 |
description: 'dokumen',
|
| 281 |
tone: 'danger',
|
| 282 |
+
value: isLoading ? (
|
| 283 |
+
<LoadingDots className="lecturer-metric-loading" />
|
| 284 |
+
) : (
|
| 285 |
+
String(failedDocumentCount)
|
| 286 |
+
),
|
| 287 |
},
|
| 288 |
];
|
| 289 |
}, [isLoading, knowledgeBases]);
|
|
|
|
| 377 |
className="lecturer-table-empty"
|
| 378 |
colSpan={4}
|
| 379 |
>
|
| 380 |
+
<LoadingIndicator label="Memuat knowledge base" />
|
| 381 |
</td>
|
| 382 |
</tr>
|
| 383 |
)}
|
|
|
|
| 544 |
disabled={isCreatingCourse}
|
| 545 |
type="submit"
|
| 546 |
>
|
| 547 |
+
{isCreatingCourse ? (
|
| 548 |
+
<LoadingIndicator
|
| 549 |
+
label="Menyimpan"
|
| 550 |
+
spinnerClassName="size-3.5"
|
| 551 |
+
/>
|
| 552 |
+
) : (
|
| 553 |
+
'Simpan'
|
| 554 |
+
)}
|
| 555 |
</Button>
|
| 556 |
</DialogFooter>
|
| 557 |
</form>
|
resources/js/pages/mahasiswa-chat.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
| 11 |
StudentShell,
|
| 12 |
useStoredStudentName,
|
| 13 |
} from '@/components/student/student-shell';
|
|
|
|
| 14 |
import { getStoredAuthToken } from '@/lib/auth';
|
| 15 |
import type {
|
| 16 |
ChatHistoryResponse,
|
|
@@ -455,7 +456,7 @@ export default function MahasiswaChat({ sessionId }: MahasiswaChatProps) {
|
|
| 455 |
|
| 456 |
{isLoading && messages.length === 0 && !isProcessing ? (
|
| 457 |
<p className="student-chat-loading">
|
| 458 |
-
Memuat percakapan
|
| 459 |
</p>
|
| 460 |
) : (
|
| 461 |
<div className="student-chat-messages">
|
|
@@ -472,7 +473,10 @@ export default function MahasiswaChat({ sessionId }: MahasiswaChatProps) {
|
|
| 472 |
<Bot className="size-4" />
|
| 473 |
</span>
|
| 474 |
<div className="student-message-bubble student-message-bubble--typing">
|
| 475 |
-
|
|
|
|
|
|
|
|
|
|
| 476 |
</div>
|
| 477 |
</article>
|
| 478 |
)}
|
|
|
|
| 11 |
StudentShell,
|
| 12 |
useStoredStudentName,
|
| 13 |
} from '@/components/student/student-shell';
|
| 14 |
+
import { LoadingIndicator } from '@/components/ui/loading-indicator';
|
| 15 |
import { getStoredAuthToken } from '@/lib/auth';
|
| 16 |
import type {
|
| 17 |
ChatHistoryResponse,
|
|
|
|
| 456 |
|
| 457 |
{isLoading && messages.length === 0 && !isProcessing ? (
|
| 458 |
<p className="student-chat-loading">
|
| 459 |
+
<LoadingIndicator label="Memuat percakapan" />
|
| 460 |
</p>
|
| 461 |
) : (
|
| 462 |
<div className="student-chat-messages">
|
|
|
|
| 473 |
<Bot className="size-4" />
|
| 474 |
</span>
|
| 475 |
<div className="student-message-bubble student-message-bubble--typing">
|
| 476 |
+
<LoadingIndicator
|
| 477 |
+
label="Thinking"
|
| 478 |
+
showSpinner={false}
|
| 479 |
+
/>
|
| 480 |
</div>
|
| 481 |
</article>
|
| 482 |
)}
|