Spaces:
Sleeping
Sleeping
fix: qa report all
Browse files- .env.example +1 -1
- public/logo-sevima.png +0 -0
- public/logo-sevima.svg +34 -0
- resources/css/app.css +4 -4
- resources/js/components/admin/admin-llm-form.tsx +17 -6
- resources/js/components/admin/admin-shell.tsx +1 -1
- resources/js/components/admin/admin-user-management.tsx +19 -10
- resources/js/components/auth/register-role-form.tsx +9 -9
- resources/js/lib/rag-client.ts +17 -2
- resources/js/pages/dosen-detail.tsx +38 -13
- resources/js/pages/welcome.tsx +1 -1
- resources/views/app.blade.php +3 -3
- sevima-raghub.json +124 -381
.env.example
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
APP_NAME=
|
| 2 |
APP_ENV=local
|
| 3 |
APP_KEY=
|
| 4 |
APP_DEBUG=true
|
|
|
|
| 1 |
+
APP_NAME="RAG Hub"
|
| 2 |
APP_ENV=local
|
| 3 |
APP_KEY=
|
| 4 |
APP_DEBUG=true
|
public/logo-sevima.png
ADDED
|
public/logo-sevima.svg
ADDED
|
|
resources/css/app.css
CHANGED
|
@@ -501,7 +501,7 @@ @layer components {
|
|
| 501 |
}
|
| 502 |
|
| 503 |
.register-card {
|
| 504 |
-
@apply w-full max-w-[
|
| 505 |
background-color: var(--register-card-bg) !important;
|
| 506 |
border-color: var(--register-card-border) !important;
|
| 507 |
box-shadow: var(--register-card-shadow) !important;
|
|
@@ -570,7 +570,7 @@ @layer components {
|
|
| 570 |
}
|
| 571 |
|
| 572 |
.register-error {
|
| 573 |
-
@apply text-
|
| 574 |
color: var(--register-danger);
|
| 575 |
}
|
| 576 |
|
|
@@ -590,7 +590,7 @@ @layer components {
|
|
| 590 |
}
|
| 591 |
|
| 592 |
.login-card {
|
| 593 |
-
@apply max-w-[
|
| 594 |
}
|
| 595 |
|
| 596 |
.login-card-content {
|
|
@@ -1632,7 +1632,7 @@ @layer components {
|
|
| 1632 |
}
|
| 1633 |
|
| 1634 |
.lecturer-user-button {
|
| 1635 |
-
@apply mb-3 w-fit gap-2 rounded-[6px] shadow-none;
|
| 1636 |
background-color: var(--lecturer-surface-soft);
|
| 1637 |
color: var(--lecturer-text);
|
| 1638 |
height: auto !important;
|
|
|
|
| 501 |
}
|
| 502 |
|
| 503 |
.register-card {
|
| 504 |
+
@apply w-full max-w-[460px] gap-0 rounded-[17px] border py-0;
|
| 505 |
background-color: var(--register-card-bg) !important;
|
| 506 |
border-color: var(--register-card-border) !important;
|
| 507 |
box-shadow: var(--register-card-shadow) !important;
|
|
|
|
| 570 |
}
|
| 571 |
|
| 572 |
.register-error {
|
| 573 |
+
@apply text-xs leading-snug;
|
| 574 |
color: var(--register-danger);
|
| 575 |
}
|
| 576 |
|
|
|
|
| 590 |
}
|
| 591 |
|
| 592 |
.login-card {
|
| 593 |
+
@apply max-w-[460px];
|
| 594 |
}
|
| 595 |
|
| 596 |
.login-card-content {
|
|
|
|
| 1632 |
}
|
| 1633 |
|
| 1634 |
.lecturer-user-button {
|
| 1635 |
+
@apply mb-3 w-fit gap-2 rounded-[6px] shadow-none cursor-default;
|
| 1636 |
background-color: var(--lecturer-surface-soft);
|
| 1637 |
color: var(--lecturer-text);
|
| 1638 |
height: auto !important;
|
resources/js/components/admin/admin-llm-form.tsx
CHANGED
|
@@ -16,7 +16,8 @@ import {
|
|
| 16 |
import { Spinner } from '@/components/ui/spinner';
|
| 17 |
import { stringValue } from '@/lib/admin';
|
| 18 |
import { cn } from '@/lib/utils';
|
| 19 |
-
import { patchLlmConfig } from '@/lib/rag-client';
|
|
|
|
| 20 |
import type { LlmConfig, LlmForm } from '@/types/admin';
|
| 21 |
|
| 22 |
const PROVIDER_CONFIG: Record<
|
|
@@ -292,6 +293,13 @@ export function AdminLlmForm({ llm }: { llm?: LlmConfig | null }) {
|
|
| 292 |
});
|
| 293 |
const [isSubmitting, setIsSubmitting] = useState(false);
|
| 294 |
const [error, setError] = useState<string | undefined>();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
function setField<K extends keyof LlmForm>(
|
| 297 |
key: K,
|
|
@@ -301,13 +309,13 @@ export function AdminLlmForm({ llm }: { llm?: LlmConfig | null }) {
|
|
| 301 |
}
|
| 302 |
|
| 303 |
function handleProviderChange(provider: string): void {
|
| 304 |
-
const config =
|
| 305 |
setFormData((prev) => ({
|
| 306 |
...prev,
|
| 307 |
provider,
|
| 308 |
model: '',
|
| 309 |
reasoning_effort: '',
|
| 310 |
-
base_url: config?.base_urls[0] ?? '',
|
| 311 |
}));
|
| 312 |
}
|
| 313 |
|
|
@@ -359,7 +367,7 @@ export function AdminLlmForm({ llm }: { llm?: LlmConfig | null }) {
|
|
| 359 |
}
|
| 360 |
}
|
| 361 |
|
| 362 |
-
const providerConfig =
|
| 363 |
|
| 364 |
return (
|
| 365 |
<Card className="border-(--lecturer-border) bg-(--lecturer-surface)">
|
|
@@ -386,8 +394,11 @@ export function AdminLlmForm({ llm }: { llm?: LlmConfig | null }) {
|
|
| 386 |
<SelectValue placeholder="Pilih provider" />
|
| 387 |
</SelectTrigger>
|
| 388 |
<SelectContent>
|
| 389 |
-
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
| 391 |
</SelectContent>
|
| 392 |
</Select>
|
| 393 |
</Field>
|
|
|
|
| 16 |
import { Spinner } from '@/components/ui/spinner';
|
| 17 |
import { stringValue } from '@/lib/admin';
|
| 18 |
import { cn } from '@/lib/utils';
|
| 19 |
+
import { getLlmProviderOptions, patchLlmConfig } from '@/lib/rag-client';
|
| 20 |
+
import type { LlmProviderConfig } from '@/lib/rag-client';
|
| 21 |
import type { LlmConfig, LlmForm } from '@/types/admin';
|
| 22 |
|
| 23 |
const PROVIDER_CONFIG: Record<
|
|
|
|
| 293 |
});
|
| 294 |
const [isSubmitting, setIsSubmitting] = useState(false);
|
| 295 |
const [error, setError] = useState<string | undefined>();
|
| 296 |
+
const [providerOptions, setProviderOptions] = useState<Record<string, LlmProviderConfig>>(PROVIDER_CONFIG);
|
| 297 |
+
|
| 298 |
+
useEffect(() => {
|
| 299 |
+
getLlmProviderOptions()
|
| 300 |
+
.then(setProviderOptions)
|
| 301 |
+
.catch(() => {}); // fallback ke PROVIDER_CONFIG jika gagal
|
| 302 |
+
}, []);
|
| 303 |
|
| 304 |
function setField<K extends keyof LlmForm>(
|
| 305 |
key: K,
|
|
|
|
| 309 |
}
|
| 310 |
|
| 311 |
function handleProviderChange(provider: string): void {
|
| 312 |
+
const config = providerOptions[provider];
|
| 313 |
setFormData((prev) => ({
|
| 314 |
...prev,
|
| 315 |
provider,
|
| 316 |
model: '',
|
| 317 |
reasoning_effort: '',
|
| 318 |
+
base_url: config?.base_urls?.[0] ?? '',
|
| 319 |
}));
|
| 320 |
}
|
| 321 |
|
|
|
|
| 367 |
}
|
| 368 |
}
|
| 369 |
|
| 370 |
+
const providerConfig = providerOptions[formData.provider];
|
| 371 |
|
| 372 |
return (
|
| 373 |
<Card className="border-(--lecturer-border) bg-(--lecturer-surface)">
|
|
|
|
| 394 |
<SelectValue placeholder="Pilih provider" />
|
| 395 |
</SelectTrigger>
|
| 396 |
<SelectContent>
|
| 397 |
+
{Object.keys(providerOptions).map((p) => (
|
| 398 |
+
<SelectItem key={p} value={p}>
|
| 399 |
+
{p.charAt(0).toUpperCase() + p.slice(1)}
|
| 400 |
+
</SelectItem>
|
| 401 |
+
))}
|
| 402 |
</SelectContent>
|
| 403 |
</Select>
|
| 404 |
</Field>
|
resources/js/components/admin/admin-shell.tsx
CHANGED
|
@@ -98,7 +98,7 @@ function getInitials(name: string): string {
|
|
| 98 |
|
| 99 |
function confirmLogout(): void {
|
| 100 |
clearAuthSession();
|
| 101 |
-
router.visit(adminLogin());
|
| 102 |
}
|
| 103 |
|
| 104 |
function AdminSidebarBrand() {
|
|
|
|
| 98 |
|
| 99 |
function confirmLogout(): void {
|
| 100 |
clearAuthSession();
|
| 101 |
+
router.visit(adminLogin.url());
|
| 102 |
}
|
| 103 |
|
| 104 |
function AdminSidebarBrand() {
|
resources/js/components/admin/admin-user-management.tsx
CHANGED
|
@@ -80,7 +80,7 @@ export function AdminUserCreateCard({
|
|
| 80 |
setError(undefined);
|
| 81 |
|
| 82 |
try {
|
| 83 |
-
const payload =
|
| 84 |
const created = await createAdminUser(payload);
|
| 85 |
setFormData(emptyUserForm);
|
| 86 |
onCreated?.(created);
|
|
@@ -192,7 +192,7 @@ export function AdminUserManagement({
|
|
| 192 |
setCreateError(undefined);
|
| 193 |
|
| 194 |
try {
|
| 195 |
-
const payload =
|
| 196 |
const created = await createAdminUser(payload);
|
| 197 |
setUsers((prev) => [created, ...prev]);
|
| 198 |
setCreateFormData(emptyUserForm);
|
|
@@ -219,7 +219,7 @@ export function AdminUserManagement({
|
|
| 219 |
setEditError(undefined);
|
| 220 |
|
| 221 |
try {
|
| 222 |
-
const payload =
|
| 223 |
const updated = await updateAdminUser(editingUser.id, payload);
|
| 224 |
setUsers((prev) =>
|
| 225 |
prev.map((u) => (u.id === updated.id ? updated : u)),
|
|
@@ -522,19 +522,28 @@ export function AdminUserManagement({
|
|
| 522 |
);
|
| 523 |
}
|
| 524 |
|
| 525 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
const payload: Record<string, unknown> = {
|
| 527 |
is_active: form.is_active,
|
| 528 |
-
is_superuser: form.is_superuser,
|
| 529 |
role: form.role,
|
| 530 |
};
|
| 531 |
-
if (form.name) payload.name = form.name;
|
| 532 |
if (form.email) payload.email = form.email;
|
| 533 |
-
if (form.
|
| 534 |
-
if (form.identity_number) payload.identity_number = form.identity_number;
|
| 535 |
if (form.phone) payload.phone = form.phone;
|
| 536 |
-
if (form.
|
| 537 |
-
|
| 538 |
return payload;
|
| 539 |
}
|
| 540 |
|
|
|
|
| 80 |
setError(undefined);
|
| 81 |
|
| 82 |
try {
|
| 83 |
+
const payload = buildCreatePayload(formData);
|
| 84 |
const created = await createAdminUser(payload);
|
| 85 |
setFormData(emptyUserForm);
|
| 86 |
onCreated?.(created);
|
|
|
|
| 192 |
setCreateError(undefined);
|
| 193 |
|
| 194 |
try {
|
| 195 |
+
const payload = buildCreatePayload(createFormData);
|
| 196 |
const created = await createAdminUser(payload);
|
| 197 |
setUsers((prev) => [created, ...prev]);
|
| 198 |
setCreateFormData(emptyUserForm);
|
|
|
|
| 219 |
setEditError(undefined);
|
| 220 |
|
| 221 |
try {
|
| 222 |
+
const payload = buildUpdatePayload(editFormData);
|
| 223 |
const updated = await updateAdminUser(editingUser.id, payload);
|
| 224 |
setUsers((prev) =>
|
| 225 |
prev.map((u) => (u.id === updated.id ? updated : u)),
|
|
|
|
| 522 |
);
|
| 523 |
}
|
| 524 |
|
| 525 |
+
function buildCreatePayload(form: UserForm): Record<string, unknown> {
|
| 526 |
+
// POST /auth/register — RegisterRequest schema
|
| 527 |
+
const payload: Record<string, unknown> = { role: form.role };
|
| 528 |
+
if (form.email) payload.email = form.email;
|
| 529 |
+
if (form.name) payload.name = form.name;
|
| 530 |
+
if (form.password) payload.password = form.password;
|
| 531 |
+
if (form.identity_number) payload.identity_number = form.identity_number;
|
| 532 |
+
if (form.phone) payload.phone = form.phone;
|
| 533 |
+
return payload;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
function buildUpdatePayload(form: UserForm): Record<string, unknown> {
|
| 537 |
+
// PATCH /user/{id} — PatchUser schema (additionalProperties: false)
|
| 538 |
+
// Only: email, name, phone, role, is_active, password
|
| 539 |
const payload: Record<string, unknown> = {
|
| 540 |
is_active: form.is_active,
|
|
|
|
| 541 |
role: form.role,
|
| 542 |
};
|
|
|
|
| 543 |
if (form.email) payload.email = form.email;
|
| 544 |
+
if (form.name) payload.name = form.name;
|
|
|
|
| 545 |
if (form.phone) payload.phone = form.phone;
|
| 546 |
+
if (form.password) payload.password = form.password;
|
|
|
|
| 547 |
return payload;
|
| 548 |
}
|
| 549 |
|
resources/js/components/auth/register-role-form.tsx
CHANGED
|
@@ -83,11 +83,11 @@ function validateRegisterForm(
|
|
| 83 |
const normalizedIdentityNumber = values.identity_number.trim();
|
| 84 |
|
| 85 |
if (!normalizedName) {
|
| 86 |
-
errors.name = 'Nama lengkap wajib diisi
|
| 87 |
} else if (normalizedName.length < 3) {
|
| 88 |
-
errors.name = 'Nama lengkap minimal 3 karakter
|
| 89 |
} else if (!namePattern.test(normalizedName)) {
|
| 90 |
-
errors.name = 'Nama hanya boleh berisi huruf dan tanda nama
|
| 91 |
}
|
| 92 |
|
| 93 |
if (!normalizedIdentityNumber) {
|
|
@@ -101,27 +101,27 @@ function validateRegisterForm(
|
|
| 101 |
}
|
| 102 |
|
| 103 |
if (!normalizedEmail) {
|
| 104 |
-
errors.email = 'Email wajib diisi
|
| 105 |
} else if (
|
| 106 |
normalizedEmail.includes(' ') ||
|
| 107 |
!emailPattern.test(normalizedEmail)
|
| 108 |
) {
|
| 109 |
-
errors.email = 'Format email tidak valid
|
| 110 |
}
|
| 111 |
|
| 112 |
const normalizedPhone = values.phone.trim().replace(/[\s().-]/g, '');
|
| 113 |
|
| 114 |
if (normalizedPhone && !phonePattern.test(normalizedPhone)) {
|
| 115 |
errors.phone =
|
| 116 |
-
'Nomor telepon hanya boleh angka dan opsional diawali tanda +
|
| 117 |
}
|
| 118 |
|
| 119 |
if (!values.password) {
|
| 120 |
-
errors.password = 'Password wajib diisi
|
| 121 |
} else if (values.password.length < 6) {
|
| 122 |
-
errors.password = 'Password minimal 6 karakter
|
| 123 |
} else if (/\s/.test(values.password)) {
|
| 124 |
-
errors.password = 'Password tidak boleh mengandung spasi
|
| 125 |
}
|
| 126 |
|
| 127 |
return errors;
|
|
|
|
| 83 |
const normalizedIdentityNumber = values.identity_number.trim();
|
| 84 |
|
| 85 |
if (!normalizedName) {
|
| 86 |
+
errors.name = 'Nama lengkap wajib diisi';
|
| 87 |
} else if (normalizedName.length < 3) {
|
| 88 |
+
errors.name = 'Nama lengkap minimal 3 karakter';
|
| 89 |
} else if (!namePattern.test(normalizedName)) {
|
| 90 |
+
errors.name = 'Nama hanya boleh berisi huruf dan tanda nama';
|
| 91 |
}
|
| 92 |
|
| 93 |
if (!normalizedIdentityNumber) {
|
|
|
|
| 101 |
}
|
| 102 |
|
| 103 |
if (!normalizedEmail) {
|
| 104 |
+
errors.email = 'Email wajib diisi';
|
| 105 |
} else if (
|
| 106 |
normalizedEmail.includes(' ') ||
|
| 107 |
!emailPattern.test(normalizedEmail)
|
| 108 |
) {
|
| 109 |
+
errors.email = 'Format email tidak valid';
|
| 110 |
}
|
| 111 |
|
| 112 |
const normalizedPhone = values.phone.trim().replace(/[\s().-]/g, '');
|
| 113 |
|
| 114 |
if (normalizedPhone && !phonePattern.test(normalizedPhone)) {
|
| 115 |
errors.phone =
|
| 116 |
+
'Nomor telepon hanya boleh angka dan opsional diawali tanda +';
|
| 117 |
}
|
| 118 |
|
| 119 |
if (!values.password) {
|
| 120 |
+
errors.password = 'Password wajib diisi';
|
| 121 |
} else if (values.password.length < 6) {
|
| 122 |
+
errors.password = 'Password minimal 6 karakter';
|
| 123 |
} else if (/\s/.test(values.password)) {
|
| 124 |
+
errors.password = 'Password tidak boleh mengandung spasi';
|
| 125 |
}
|
| 126 |
|
| 127 |
return errors;
|
resources/js/lib/rag-client.ts
CHANGED
|
@@ -329,10 +329,20 @@ export function getRagConfig(): Promise<RagConfig> {
|
|
| 329 |
return ragGet('/admin/rag/config');
|
| 330 |
}
|
| 331 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
export function getLlmConfig(): Promise<LlmConfig> {
|
| 333 |
return ragGet('/admin/rag/config/llm');
|
| 334 |
}
|
| 335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
export function patchLlmConfig(
|
| 337 |
payload: Record<string, unknown>,
|
| 338 |
): Promise<LlmConfig> {
|
|
@@ -376,10 +386,15 @@ export function listAdminUsers(
|
|
| 376 |
return ragGet(path);
|
| 377 |
}
|
| 378 |
|
| 379 |
-
export function createAdminUser(
|
| 380 |
payload: Record<string, unknown>,
|
| 381 |
): Promise<AdminUser> {
|
| 382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
}
|
| 384 |
|
| 385 |
export function updateAdminUser(
|
|
|
|
| 329 |
return ragGet('/admin/rag/config');
|
| 330 |
}
|
| 331 |
|
| 332 |
+
export type LlmProviderConfig = {
|
| 333 |
+
base_urls?: string[];
|
| 334 |
+
models?: string[];
|
| 335 |
+
reasoning_efforts?: string[];
|
| 336 |
+
};
|
| 337 |
+
|
| 338 |
export function getLlmConfig(): Promise<LlmConfig> {
|
| 339 |
return ragGet('/admin/rag/config/llm');
|
| 340 |
}
|
| 341 |
|
| 342 |
+
export function getLlmProviderOptions(): Promise<Record<string, LlmProviderConfig>> {
|
| 343 |
+
return ragGet('/admin/rag/config/llm/options');
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
export function patchLlmConfig(
|
| 347 |
payload: Record<string, unknown>,
|
| 348 |
): Promise<LlmConfig> {
|
|
|
|
| 386 |
return ragGet(path);
|
| 387 |
}
|
| 388 |
|
| 389 |
+
export async function createAdminUser(
|
| 390 |
payload: Record<string, unknown>,
|
| 391 |
): Promise<AdminUser> {
|
| 392 |
+
const response = await ragJson<{ message: string; user: AdminUser }>(
|
| 393 |
+
'/auth/register',
|
| 394 |
+
'POST',
|
| 395 |
+
payload,
|
| 396 |
+
);
|
| 397 |
+
return response.user;
|
| 398 |
}
|
| 399 |
|
| 400 |
export function updateAdminUser(
|
resources/js/pages/dosen-detail.tsx
CHANGED
|
@@ -11,10 +11,12 @@ import {
|
|
| 11 |
} from 'lucide-react';
|
| 12 |
import type { ChangeEvent, DragEvent, FormEvent } from 'react';
|
| 13 |
import { useEffect, useRef, useState } from 'react';
|
|
|
|
| 14 |
|
| 15 |
import { DosenShell } from '@/components/dosen/dosen-shell';
|
| 16 |
import { Button } from '@/components/ui/button';
|
| 17 |
import { Card, CardContent } from '@/components/ui/card';
|
|
|
|
| 18 |
import {
|
| 19 |
Dialog,
|
| 20 |
DialogContent,
|
|
@@ -177,6 +179,7 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 177 |
const [editFormError, setEditFormError] = useState<string>();
|
| 178 |
const [isUpdatingCourse, setIsUpdatingCourse] = useState(false);
|
| 179 |
const [isDeletingCourse, setIsDeletingCourse] = useState(false);
|
|
|
|
| 180 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 181 |
|
| 182 |
const hasProcessingDocuments = documents.some(
|
|
@@ -184,13 +187,12 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 184 |
);
|
| 185 |
|
| 186 |
useEffect(() => {
|
| 187 |
-
|
| 188 |
-
.then(
|
| 189 |
-
.
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
.
|
| 193 |
-
.catch(() => {});
|
| 194 |
}, [courseId]);
|
| 195 |
|
| 196 |
useEffect(() => {
|
|
@@ -290,6 +292,7 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 290 |
|
| 291 |
try {
|
| 292 |
await deleteCourse(courseId);
|
|
|
|
| 293 |
router.visit(dosen());
|
| 294 |
} catch (e) {
|
| 295 |
setBackendMessage(
|
|
@@ -338,6 +341,30 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 338 |
|
| 339 |
<DosenShell>
|
| 340 |
<main className="lecturer-detail-page">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
<section className="lecturer-detail-header">
|
| 342 |
<button
|
| 343 |
aria-label="Kembali ke dashboard dosen"
|
|
@@ -462,6 +489,8 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 462 |
</div>
|
| 463 |
</section>
|
| 464 |
</section>
|
|
|
|
|
|
|
| 465 |
</main>
|
| 466 |
|
| 467 |
<Dialog
|
|
@@ -570,18 +599,14 @@ export default function DosenDetail({ courseId }: DosenDetailProps) {
|
|
| 570 |
<DialogContent className="lecturer-course-dialog">
|
| 571 |
<DialogHeader>
|
| 572 |
<DialogTitle>Hapus Mata Kuliah</DialogTitle>
|
| 573 |
-
<DialogDescription>
|
| 574 |
-
Tindakan ini akan menghapus knowledge base dari
|
| 575 |
-
backend.
|
| 576 |
-
</DialogDescription>
|
| 577 |
</DialogHeader>
|
| 578 |
|
| 579 |
<div className="lecturer-course-form">
|
| 580 |
<p className="lecturer-delete-copy">
|
| 581 |
Mata kuliah{' '}
|
| 582 |
<strong>{course?.title ?? title}</strong> akan
|
| 583 |
-
dihapus. Pastikan data ini memang sudah
|
| 584 |
-
|
| 585 |
</p>
|
| 586 |
|
| 587 |
<DialogFooter className="lecturer-course-footer">
|
|
|
|
| 11 |
} from 'lucide-react';
|
| 12 |
import type { ChangeEvent, DragEvent, FormEvent } from 'react';
|
| 13 |
import { useEffect, useRef, useState } from 'react';
|
| 14 |
+
import { toast } from 'sonner';
|
| 15 |
|
| 16 |
import { DosenShell } from '@/components/dosen/dosen-shell';
|
| 17 |
import { Button } from '@/components/ui/button';
|
| 18 |
import { Card, CardContent } from '@/components/ui/card';
|
| 19 |
+
import { Skeleton } from '@/components/ui/skeleton';
|
| 20 |
import {
|
| 21 |
Dialog,
|
| 22 |
DialogContent,
|
|
|
|
| 179 |
const [editFormError, setEditFormError] = useState<string>();
|
| 180 |
const [isUpdatingCourse, setIsUpdatingCourse] = useState(false);
|
| 181 |
const [isDeletingCourse, setIsDeletingCourse] = useState(false);
|
| 182 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 183 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 184 |
|
| 185 |
const hasProcessingDocuments = documents.some(
|
|
|
|
| 187 |
);
|
| 188 |
|
| 189 |
useEffect(() => {
|
| 190 |
+
Promise.all([
|
| 191 |
+
getCourse(courseId).then(setCourse),
|
| 192 |
+
listCourseDocuments(courseId).then((r) => setDocuments(r.data)).catch(() => {}),
|
| 193 |
+
])
|
| 194 |
+
.catch((e: Error) => setBackendMessage(e.message))
|
| 195 |
+
.finally(() => setIsLoading(false));
|
|
|
|
| 196 |
}, [courseId]);
|
| 197 |
|
| 198 |
useEffect(() => {
|
|
|
|
| 292 |
|
| 293 |
try {
|
| 294 |
await deleteCourse(courseId);
|
| 295 |
+
toast.success('Mata kuliah berhasil dihapus');
|
| 296 |
router.visit(dosen());
|
| 297 |
} catch (e) {
|
| 298 |
setBackendMessage(
|
|
|
|
| 341 |
|
| 342 |
<DosenShell>
|
| 343 |
<main className="lecturer-detail-page">
|
| 344 |
+
{isLoading ? (
|
| 345 |
+
<>
|
| 346 |
+
<section className="lecturer-detail-header">
|
| 347 |
+
<Skeleton className="size-8 rounded-full shrink-0" />
|
| 348 |
+
<div className="lecturer-detail-heading flex-1">
|
| 349 |
+
<Skeleton className="h-7 w-48" />
|
| 350 |
+
<Skeleton className="mt-2 h-4 w-72" />
|
| 351 |
+
</div>
|
| 352 |
+
<div className="lecturer-detail-actions">
|
| 353 |
+
<Skeleton className="h-9 w-20" />
|
| 354 |
+
<Skeleton className="h-9 w-9" />
|
| 355 |
+
</div>
|
| 356 |
+
</section>
|
| 357 |
+
<section className="lecturer-detail-content">
|
| 358 |
+
<Skeleton className="h-36 w-full rounded-xl" />
|
| 359 |
+
<div className="mt-4 grid gap-3">
|
| 360 |
+
{Array.from({ length: 3 }).map((_, i) => (
|
| 361 |
+
<Skeleton className="h-20 w-full rounded-xl" key={i} />
|
| 362 |
+
))}
|
| 363 |
+
</div>
|
| 364 |
+
</section>
|
| 365 |
+
</>
|
| 366 |
+
) : (
|
| 367 |
+
<>
|
| 368 |
<section className="lecturer-detail-header">
|
| 369 |
<button
|
| 370 |
aria-label="Kembali ke dashboard dosen"
|
|
|
|
| 489 |
</div>
|
| 490 |
</section>
|
| 491 |
</section>
|
| 492 |
+
</>
|
| 493 |
+
)}
|
| 494 |
</main>
|
| 495 |
|
| 496 |
<Dialog
|
|
|
|
| 599 |
<DialogContent className="lecturer-course-dialog">
|
| 600 |
<DialogHeader>
|
| 601 |
<DialogTitle>Hapus Mata Kuliah</DialogTitle>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
</DialogHeader>
|
| 603 |
|
| 604 |
<div className="lecturer-course-form">
|
| 605 |
<p className="lecturer-delete-copy">
|
| 606 |
Mata kuliah{' '}
|
| 607 |
<strong>{course?.title ?? title}</strong> akan
|
| 608 |
+
dihapus permanen. Pastikan data ini memang sudah
|
| 609 |
+
tidak digunakan.
|
| 610 |
</p>
|
| 611 |
|
| 612 |
<DialogFooter className="lecturer-course-footer">
|
resources/js/pages/welcome.tsx
CHANGED
|
@@ -48,7 +48,7 @@ function AuthEntryCard({
|
|
| 48 |
export default function Welcome({ canRegister = true }: WelcomeProps) {
|
| 49 |
return (
|
| 50 |
<>
|
| 51 |
-
<Head title="
|
| 52 |
|
| 53 |
<main className="welcome-shell">
|
| 54 |
<AppearanceToggle />
|
|
|
|
| 48 |
export default function Welcome({ canRegister = true }: WelcomeProps) {
|
| 49 |
return (
|
| 50 |
<>
|
| 51 |
+
<Head title="Selamat Datang" />
|
| 52 |
|
| 53 |
<main className="welcome-shell">
|
| 54 |
<AppearanceToggle />
|
resources/views/app.blade.php
CHANGED
|
@@ -31,9 +31,9 @@
|
|
| 31 |
}
|
| 32 |
</style>
|
| 33 |
|
| 34 |
-
<link rel="icon" href="/
|
| 35 |
-
<link rel="icon" href="/
|
| 36 |
-
<link rel="apple-touch-icon" href="/
|
| 37 |
|
| 38 |
@fonts
|
| 39 |
|
|
|
|
| 31 |
}
|
| 32 |
</style>
|
| 33 |
|
| 34 |
+
<link rel="icon" href="/logo-sevima.svg" type="image/svg+xml">
|
| 35 |
+
<link rel="icon" href="/logo-sevima.png" type="image/png">
|
| 36 |
+
<link rel="apple-touch-icon" href="/logo-sevima.png">
|
| 37 |
|
| 38 |
@fonts
|
| 39 |
|
sevima-raghub.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
| 23 |
},
|
| 24 |
"/api/v1/auth/register": {
|
| 25 |
"post": {
|
| 26 |
-
"tags": ["v1", "auth"],
|
| 27 |
"summary": "Register",
|
| 28 |
"description": "Register a new user (student or lecturer). Source: openapi-spec /auth/register",
|
| 29 |
"operationId": "register_api_v1_auth_register_post",
|
|
@@ -182,31 +182,6 @@
|
|
| 182 |
}
|
| 183 |
}
|
| 184 |
},
|
| 185 |
-
"/api/v1/auth/api-keys": {
|
| 186 |
-
"post": {
|
| 187 |
-
"tags": ["v1", "auth"],
|
| 188 |
-
"summary": "Generate Api Key",
|
| 189 |
-
"description": "Generate a new API key. Restricted to lecturers and admins. Source: openapi-spec /auth/api-keys",
|
| 190 |
-
"operationId": "generate_api_key_api_v1_auth_api_keys_post",
|
| 191 |
-
"responses": {
|
| 192 |
-
"201": {
|
| 193 |
-
"description": "Successful Response",
|
| 194 |
-
"content": {
|
| 195 |
-
"application/json": {
|
| 196 |
-
"schema": {
|
| 197 |
-
"$ref": "#/components/schemas/ApiKeyGenerateResponse"
|
| 198 |
-
}
|
| 199 |
-
}
|
| 200 |
-
}
|
| 201 |
-
}
|
| 202 |
-
},
|
| 203 |
-
"security": [
|
| 204 |
-
{
|
| 205 |
-
"JWTBearer": []
|
| 206 |
-
}
|
| 207 |
-
]
|
| 208 |
-
}
|
| 209 |
-
},
|
| 210 |
"/api/v1/documents/upload": {
|
| 211 |
"post": {
|
| 212 |
"tags": ["v1", "documents"],
|
|
@@ -539,131 +514,6 @@
|
|
| 539 |
"tags": ["v1", "user"],
|
| 540 |
"summary": "Get User List",
|
| 541 |
"operationId": "get_user_list_api_v1_user_get",
|
| 542 |
-
"parameters": [
|
| 543 |
-
{
|
| 544 |
-
"required": false,
|
| 545 |
-
"schema": {
|
| 546 |
-
"type": "string",
|
| 547 |
-
"title": "Email"
|
| 548 |
-
},
|
| 549 |
-
"name": "email",
|
| 550 |
-
"in": "query"
|
| 551 |
-
},
|
| 552 |
-
{
|
| 553 |
-
"required": false,
|
| 554 |
-
"schema": {
|
| 555 |
-
"type": "string",
|
| 556 |
-
"title": "User Token"
|
| 557 |
-
},
|
| 558 |
-
"name": "user_token",
|
| 559 |
-
"in": "query"
|
| 560 |
-
},
|
| 561 |
-
{
|
| 562 |
-
"required": false,
|
| 563 |
-
"schema": {
|
| 564 |
-
"type": "string",
|
| 565 |
-
"title": "Name"
|
| 566 |
-
},
|
| 567 |
-
"name": "name",
|
| 568 |
-
"in": "query"
|
| 569 |
-
},
|
| 570 |
-
{
|
| 571 |
-
"required": false,
|
| 572 |
-
"schema": {
|
| 573 |
-
"type": "string",
|
| 574 |
-
"title": "Phone"
|
| 575 |
-
},
|
| 576 |
-
"name": "phone",
|
| 577 |
-
"in": "query"
|
| 578 |
-
},
|
| 579 |
-
{
|
| 580 |
-
"required": false,
|
| 581 |
-
"schema": {
|
| 582 |
-
"$ref": "#/components/schemas/app__schema__user_schema__Role"
|
| 583 |
-
},
|
| 584 |
-
"name": "role",
|
| 585 |
-
"in": "query"
|
| 586 |
-
},
|
| 587 |
-
{
|
| 588 |
-
"required": false,
|
| 589 |
-
"schema": {
|
| 590 |
-
"type": "string",
|
| 591 |
-
"title": "Identity Number"
|
| 592 |
-
},
|
| 593 |
-
"name": "identity_number",
|
| 594 |
-
"in": "query"
|
| 595 |
-
},
|
| 596 |
-
{
|
| 597 |
-
"required": false,
|
| 598 |
-
"schema": {
|
| 599 |
-
"type": "boolean",
|
| 600 |
-
"title": "Is Active"
|
| 601 |
-
},
|
| 602 |
-
"name": "is_active",
|
| 603 |
-
"in": "query"
|
| 604 |
-
},
|
| 605 |
-
{
|
| 606 |
-
"required": false,
|
| 607 |
-
"schema": {
|
| 608 |
-
"type": "boolean",
|
| 609 |
-
"title": "Is Superuser"
|
| 610 |
-
},
|
| 611 |
-
"name": "is_superuser",
|
| 612 |
-
"in": "query"
|
| 613 |
-
},
|
| 614 |
-
{
|
| 615 |
-
"required": false,
|
| 616 |
-
"schema": {
|
| 617 |
-
"type": "string",
|
| 618 |
-
"title": "Ordering"
|
| 619 |
-
},
|
| 620 |
-
"name": "ordering",
|
| 621 |
-
"in": "query"
|
| 622 |
-
},
|
| 623 |
-
{
|
| 624 |
-
"required": false,
|
| 625 |
-
"schema": {
|
| 626 |
-
"type": "integer",
|
| 627 |
-
"title": "Page"
|
| 628 |
-
},
|
| 629 |
-
"name": "page",
|
| 630 |
-
"in": "query"
|
| 631 |
-
},
|
| 632 |
-
{
|
| 633 |
-
"required": false,
|
| 634 |
-
"schema": {
|
| 635 |
-
"anyOf": [
|
| 636 |
-
{
|
| 637 |
-
"type": "integer"
|
| 638 |
-
},
|
| 639 |
-
{
|
| 640 |
-
"type": "string"
|
| 641 |
-
}
|
| 642 |
-
],
|
| 643 |
-
"title": "Page Size"
|
| 644 |
-
},
|
| 645 |
-
"name": "page_size",
|
| 646 |
-
"in": "query"
|
| 647 |
-
},
|
| 648 |
-
{
|
| 649 |
-
"required": false,
|
| 650 |
-
"schema": {
|
| 651 |
-
"type": "string",
|
| 652 |
-
"title": "Email Eq"
|
| 653 |
-
},
|
| 654 |
-
"name": "email__eq",
|
| 655 |
-
"in": "query"
|
| 656 |
-
},
|
| 657 |
-
{
|
| 658 |
-
"required": false,
|
| 659 |
-
"schema": {
|
| 660 |
-
"type": "string",
|
| 661 |
-
"title": "Identity Number Eq"
|
| 662 |
-
},
|
| 663 |
-
"name": "identity_number__eq",
|
| 664 |
-
"in": "query"
|
| 665 |
-
}
|
| 666 |
-
],
|
| 667 |
"responses": {
|
| 668 |
"200": {
|
| 669 |
"description": "Successful Response",
|
|
@@ -674,61 +524,6 @@
|
|
| 674 |
}
|
| 675 |
}
|
| 676 |
}
|
| 677 |
-
},
|
| 678 |
-
"422": {
|
| 679 |
-
"description": "Validation Error",
|
| 680 |
-
"content": {
|
| 681 |
-
"application/json": {
|
| 682 |
-
"schema": {
|
| 683 |
-
"$ref": "#/components/schemas/HTTPValidationError"
|
| 684 |
-
}
|
| 685 |
-
}
|
| 686 |
-
}
|
| 687 |
-
}
|
| 688 |
-
},
|
| 689 |
-
"security": [
|
| 690 |
-
{
|
| 691 |
-
"JWTBearer": []
|
| 692 |
-
},
|
| 693 |
-
{
|
| 694 |
-
"JWTBearer": []
|
| 695 |
-
}
|
| 696 |
-
]
|
| 697 |
-
},
|
| 698 |
-
"post": {
|
| 699 |
-
"tags": ["v1", "user"],
|
| 700 |
-
"summary": "Create User",
|
| 701 |
-
"operationId": "create_user_api_v1_user_post",
|
| 702 |
-
"requestBody": {
|
| 703 |
-
"content": {
|
| 704 |
-
"application/json": {
|
| 705 |
-
"schema": {
|
| 706 |
-
"$ref": "#/components/schemas/UpsertUser"
|
| 707 |
-
}
|
| 708 |
-
}
|
| 709 |
-
},
|
| 710 |
-
"required": true
|
| 711 |
-
},
|
| 712 |
-
"responses": {
|
| 713 |
-
"200": {
|
| 714 |
-
"description": "Successful Response",
|
| 715 |
-
"content": {
|
| 716 |
-
"application/json": {
|
| 717 |
-
"schema": {
|
| 718 |
-
"$ref": "#/components/schemas/User"
|
| 719 |
-
}
|
| 720 |
-
}
|
| 721 |
-
}
|
| 722 |
-
},
|
| 723 |
-
"422": {
|
| 724 |
-
"description": "Validation Error",
|
| 725 |
-
"content": {
|
| 726 |
-
"application/json": {
|
| 727 |
-
"schema": {
|
| 728 |
-
"$ref": "#/components/schemas/HTTPValidationError"
|
| 729 |
-
}
|
| 730 |
-
}
|
| 731 |
-
}
|
| 732 |
}
|
| 733 |
},
|
| 734 |
"security": [
|
|
@@ -804,15 +599,8 @@
|
|
| 804 |
}
|
| 805 |
],
|
| 806 |
"responses": {
|
| 807 |
-
"
|
| 808 |
-
"description": "Successful Response"
|
| 809 |
-
"content": {
|
| 810 |
-
"application/json": {
|
| 811 |
-
"schema": {
|
| 812 |
-
"$ref": "#/components/schemas/Blank"
|
| 813 |
-
}
|
| 814 |
-
}
|
| 815 |
-
}
|
| 816 |
},
|
| 817 |
"422": {
|
| 818 |
"description": "Validation Error",
|
|
@@ -853,7 +641,7 @@
|
|
| 853 |
"content": {
|
| 854 |
"application/json": {
|
| 855 |
"schema": {
|
| 856 |
-
"$ref": "#/components/schemas/
|
| 857 |
}
|
| 858 |
}
|
| 859 |
},
|
|
@@ -1266,56 +1054,11 @@
|
|
| 1266 |
]
|
| 1267 |
}
|
| 1268 |
},
|
| 1269 |
-
"/api/v1/ai/query": {
|
| 1270 |
-
"post": {
|
| 1271 |
-
"tags": ["v1", "Chat"],
|
| 1272 |
-
"summary": "Query Ai Direct",
|
| 1273 |
-
"description": "Send a direct question to AI (without session history).",
|
| 1274 |
-
"operationId": "query_ai_direct_api_v1_ai_query_post",
|
| 1275 |
-
"requestBody": {
|
| 1276 |
-
"content": {
|
| 1277 |
-
"application/json": {
|
| 1278 |
-
"schema": {
|
| 1279 |
-
"$ref": "#/components/schemas/ChatQueryRequest"
|
| 1280 |
-
}
|
| 1281 |
-
}
|
| 1282 |
-
},
|
| 1283 |
-
"required": true
|
| 1284 |
-
},
|
| 1285 |
-
"responses": {
|
| 1286 |
-
"200": {
|
| 1287 |
-
"description": "Successful Response",
|
| 1288 |
-
"content": {
|
| 1289 |
-
"application/json": {
|
| 1290 |
-
"schema": {
|
| 1291 |
-
"$ref": "#/components/schemas/ChatQueryResponse"
|
| 1292 |
-
}
|
| 1293 |
-
}
|
| 1294 |
-
}
|
| 1295 |
-
},
|
| 1296 |
-
"422": {
|
| 1297 |
-
"description": "Validation Error",
|
| 1298 |
-
"content": {
|
| 1299 |
-
"application/json": {
|
| 1300 |
-
"schema": {
|
| 1301 |
-
"$ref": "#/components/schemas/HTTPValidationError"
|
| 1302 |
-
}
|
| 1303 |
-
}
|
| 1304 |
-
}
|
| 1305 |
-
}
|
| 1306 |
-
},
|
| 1307 |
-
"security": [
|
| 1308 |
-
{
|
| 1309 |
-
"JWTBearer": []
|
| 1310 |
-
}
|
| 1311 |
-
]
|
| 1312 |
-
}
|
| 1313 |
-
},
|
| 1314 |
"/api/v1/admin/rag/config": {
|
| 1315 |
"get": {
|
| 1316 |
"tags": ["v1", "admin"],
|
| 1317 |
"summary": "Get full RAG config",
|
| 1318 |
-
"description": "Returns the current LLM, VectorDB, and Retrieval configuration from the
|
| 1319 |
"operationId": "get_full_config_api_v1_admin_rag_config_get",
|
| 1320 |
"responses": {
|
| 1321 |
"200": {
|
|
@@ -1404,6 +1147,32 @@
|
|
| 1404 |
]
|
| 1405 |
}
|
| 1406 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1407 |
"/api/v1/admin/rag/config/vector_db": {
|
| 1408 |
"get": {
|
| 1409 |
"tags": ["v1", "admin"],
|
|
@@ -1431,7 +1200,7 @@
|
|
| 1431 |
"patch": {
|
| 1432 |
"tags": ["v1", "admin"],
|
| 1433 |
"summary": "Update VectorDB config",
|
| 1434 |
-
"description": "Partially update the VectorDB configuration.
|
| 1435 |
"operationId": "update_vector_db_config_api_v1_admin_rag_config_vector_db_patch",
|
| 1436 |
"requestBody": {
|
| 1437 |
"content": {
|
|
@@ -1472,6 +1241,32 @@
|
|
| 1472 |
]
|
| 1473 |
}
|
| 1474 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1475 |
"/api/v1/admin/rag/config/retrieval": {
|
| 1476 |
"get": {
|
| 1477 |
"tags": ["v1", "admin"],
|
|
@@ -1540,6 +1335,32 @@
|
|
| 1540 |
]
|
| 1541 |
}
|
| 1542 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1543 |
"/api/v1/auth/api-key/generate": {
|
| 1544 |
"post": {
|
| 1545 |
"tags": ["v1", "auth-iframe"],
|
|
@@ -1851,23 +1672,6 @@
|
|
| 1851 |
"required": ["message", "user"],
|
| 1852 |
"title": "ActivateAccountResponse"
|
| 1853 |
},
|
| 1854 |
-
"ApiKeyGenerateResponse": {
|
| 1855 |
-
"properties": {
|
| 1856 |
-
"api_key": {
|
| 1857 |
-
"type": "string",
|
| 1858 |
-
"title": "Api Key"
|
| 1859 |
-
},
|
| 1860 |
-
"expires_at": {
|
| 1861 |
-
"type": "string",
|
| 1862 |
-
"format": "date-time",
|
| 1863 |
-
"title": "Expires At"
|
| 1864 |
-
}
|
| 1865 |
-
},
|
| 1866 |
-
"type": "object",
|
| 1867 |
-
"required": ["api_key"],
|
| 1868 |
-
"title": "ApiKeyGenerateResponse",
|
| 1869 |
-
"description": "POST /auth/api-keys â 201 response. Raw key returned ONCE."
|
| 1870 |
-
},
|
| 1871 |
"ApiKeyLoginRequest": {
|
| 1872 |
"properties": {
|
| 1873 |
"api_key": {
|
|
@@ -1880,11 +1684,6 @@
|
|
| 1880 |
"title": "ApiKeyLoginRequest",
|
| 1881 |
"description": "POST /auth/login/api-key â body."
|
| 1882 |
},
|
| 1883 |
-
"Blank": {
|
| 1884 |
-
"properties": {},
|
| 1885 |
-
"type": "object",
|
| 1886 |
-
"title": "Blank"
|
| 1887 |
-
},
|
| 1888 |
"Body_upload_document_api_v1_documents_upload_post": {
|
| 1889 |
"properties": {
|
| 1890 |
"file": {
|
|
@@ -1952,58 +1751,6 @@
|
|
| 1952 |
"title": "ChatMessageResponse",
|
| 1953 |
"description": "Mirrors the message object in OpenAPI spec."
|
| 1954 |
},
|
| 1955 |
-
"ChatQueryRequest": {
|
| 1956 |
-
"properties": {
|
| 1957 |
-
"prompt": {
|
| 1958 |
-
"type": "string",
|
| 1959 |
-
"title": "Prompt",
|
| 1960 |
-
"description": "The user\u0027s prompt text."
|
| 1961 |
-
},
|
| 1962 |
-
"course_id": {
|
| 1963 |
-
"type": "string",
|
| 1964 |
-
"title": "Course Id",
|
| 1965 |
-
"description": "UUID of the course context."
|
| 1966 |
-
}
|
| 1967 |
-
},
|
| 1968 |
-
"type": "object",
|
| 1969 |
-
"required": ["prompt", "course_id"],
|
| 1970 |
-
"title": "ChatQueryRequest",
|
| 1971 |
-
"description": "Payload for direct AI query (non-session)."
|
| 1972 |
-
},
|
| 1973 |
-
"ChatQueryResponse": {
|
| 1974 |
-
"properties": {
|
| 1975 |
-
"message_id": {
|
| 1976 |
-
"type": "string",
|
| 1977 |
-
"title": "Message Id"
|
| 1978 |
-
},
|
| 1979 |
-
"role": {
|
| 1980 |
-
"type": "string",
|
| 1981 |
-
"title": "Role",
|
| 1982 |
-
"default": "assistant"
|
| 1983 |
-
},
|
| 1984 |
-
"content": {
|
| 1985 |
-
"type": "string",
|
| 1986 |
-
"title": "Content"
|
| 1987 |
-
},
|
| 1988 |
-
"sources": {
|
| 1989 |
-
"items": {
|
| 1990 |
-
"$ref": "#/components/schemas/DocumentSourceDTO"
|
| 1991 |
-
},
|
| 1992 |
-
"type": "array",
|
| 1993 |
-
"title": "Sources",
|
| 1994 |
-
"default": []
|
| 1995 |
-
},
|
| 1996 |
-
"created_at": {
|
| 1997 |
-
"type": "string",
|
| 1998 |
-
"format": "date-time",
|
| 1999 |
-
"title": "Created At"
|
| 2000 |
-
}
|
| 2001 |
-
},
|
| 2002 |
-
"type": "object",
|
| 2003 |
-
"required": ["message_id", "content", "created_at"],
|
| 2004 |
-
"title": "ChatQueryResponse",
|
| 2005 |
-
"description": "Response format for POST /ai/query (direct query without session)."
|
| 2006 |
-
},
|
| 2007 |
"ChatRESTRequest": {
|
| 2008 |
"properties": {
|
| 2009 |
"content": {
|
|
@@ -2409,7 +2156,7 @@
|
|
| 2409 |
"reasoning_effort": {
|
| 2410 |
"type": "string",
|
| 2411 |
"title": "Reasoning Effort",
|
| 2412 |
-
"description": "
|
| 2413 |
},
|
| 2414 |
"temperature": {
|
| 2415 |
"type": "number",
|
|
@@ -2538,6 +2285,38 @@
|
|
| 2538 |
"required": ["page", "limit", "total", "total_pages"],
|
| 2539 |
"title": "PaginationDTO"
|
| 2540 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2541 |
"RAGConfigResponse": {
|
| 2542 |
"properties": {
|
| 2543 |
"path": {
|
|
@@ -2582,8 +2361,9 @@
|
|
| 2582 |
},
|
| 2583 |
"identity_number": {
|
| 2584 |
"type": "string",
|
|
|
|
| 2585 |
"title": "Identity Number",
|
| 2586 |
-
"description": "Student ID (NIM)
|
| 2587 |
}
|
| 2588 |
},
|
| 2589 |
"type": "object",
|
|
@@ -2804,43 +2584,6 @@
|
|
| 2804 |
"title": "UploadDocumentResponse",
|
| 2805 |
"description": "POST /documents/upload â 202 Accepted response.\nSource: openapi-spec.yml /documents/upload 202 schema"
|
| 2806 |
},
|
| 2807 |
-
"UpsertUser": {
|
| 2808 |
-
"properties": {
|
| 2809 |
-
"email": {
|
| 2810 |
-
"type": "string",
|
| 2811 |
-
"title": "Email"
|
| 2812 |
-
},
|
| 2813 |
-
"user_token": {
|
| 2814 |
-
"type": "string",
|
| 2815 |
-
"title": "User Token"
|
| 2816 |
-
},
|
| 2817 |
-
"name": {
|
| 2818 |
-
"type": "string",
|
| 2819 |
-
"title": "Name"
|
| 2820 |
-
},
|
| 2821 |
-
"phone": {
|
| 2822 |
-
"type": "string",
|
| 2823 |
-
"title": "Phone"
|
| 2824 |
-
},
|
| 2825 |
-
"role": {
|
| 2826 |
-
"$ref": "#/components/schemas/app__schema__user_schema__Role"
|
| 2827 |
-
},
|
| 2828 |
-
"identity_number": {
|
| 2829 |
-
"type": "string",
|
| 2830 |
-
"title": "Identity Number"
|
| 2831 |
-
},
|
| 2832 |
-
"is_active": {
|
| 2833 |
-
"type": "boolean",
|
| 2834 |
-
"title": "Is Active"
|
| 2835 |
-
},
|
| 2836 |
-
"is_superuser": {
|
| 2837 |
-
"type": "boolean",
|
| 2838 |
-
"title": "Is Superuser"
|
| 2839 |
-
}
|
| 2840 |
-
},
|
| 2841 |
-
"type": "object",
|
| 2842 |
-
"title": "UpsertUser"
|
| 2843 |
-
},
|
| 2844 |
"UpsertWhitelist": {
|
| 2845 |
"properties": {
|
| 2846 |
"source_type": {
|
|
@@ -2871,10 +2614,6 @@
|
|
| 2871 |
"type": "string",
|
| 2872 |
"title": "Email"
|
| 2873 |
},
|
| 2874 |
-
"user_token": {
|
| 2875 |
-
"type": "string",
|
| 2876 |
-
"title": "User Token"
|
| 2877 |
-
},
|
| 2878 |
"name": {
|
| 2879 |
"type": "string",
|
| 2880 |
"title": "Name"
|
|
@@ -2911,6 +2650,10 @@
|
|
| 2911 |
"type": "string",
|
| 2912 |
"format": "date-time",
|
| 2913 |
"title": "Updated At"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2914 |
}
|
| 2915 |
},
|
| 2916 |
"type": "object",
|
|
@@ -3059,7 +2802,7 @@
|
|
| 3059 |
},
|
| 3060 |
"app__schema__user_schema__Role": {
|
| 3061 |
"type": "string",
|
| 3062 |
-
"enum": ["
|
| 3063 |
"title": "Role",
|
| 3064 |
"description": "An enumeration."
|
| 3065 |
}
|
|
|
|
| 23 |
},
|
| 24 |
"/api/v1/auth/register": {
|
| 25 |
"post": {
|
| 26 |
+
"tags": ["v1", "auth", "auth", "user"],
|
| 27 |
"summary": "Register",
|
| 28 |
"description": "Register a new user (student or lecturer). Source: openapi-spec /auth/register",
|
| 29 |
"operationId": "register_api_v1_auth_register_post",
|
|
|
|
| 182 |
}
|
| 183 |
}
|
| 184 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
"/api/v1/documents/upload": {
|
| 186 |
"post": {
|
| 187 |
"tags": ["v1", "documents"],
|
|
|
|
| 514 |
"tags": ["v1", "user"],
|
| 515 |
"summary": "Get User List",
|
| 516 |
"operationId": "get_user_list_api_v1_user_get",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
"responses": {
|
| 518 |
"200": {
|
| 519 |
"description": "Successful Response",
|
|
|
|
| 524 |
}
|
| 525 |
}
|
| 526 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
}
|
| 528 |
},
|
| 529 |
"security": [
|
|
|
|
| 599 |
}
|
| 600 |
],
|
| 601 |
"responses": {
|
| 602 |
+
"204": {
|
| 603 |
+
"description": "Successful Response"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
},
|
| 605 |
"422": {
|
| 606 |
"description": "Validation Error",
|
|
|
|
| 641 |
"content": {
|
| 642 |
"application/json": {
|
| 643 |
"schema": {
|
| 644 |
+
"$ref": "#/components/schemas/PatchUser"
|
| 645 |
}
|
| 646 |
}
|
| 647 |
},
|
|
|
|
| 1054 |
]
|
| 1055 |
}
|
| 1056 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
"/api/v1/admin/rag/config": {
|
| 1058 |
"get": {
|
| 1059 |
"tags": ["v1", "admin"],
|
| 1060 |
"summary": "Get full RAG config",
|
| 1061 |
+
"description": "Returns the current LLM, VectorDB, and Retrieval configuration from the database.",
|
| 1062 |
"operationId": "get_full_config_api_v1_admin_rag_config_get",
|
| 1063 |
"responses": {
|
| 1064 |
"200": {
|
|
|
|
| 1147 |
]
|
| 1148 |
}
|
| 1149 |
},
|
| 1150 |
+
"/api/v1/admin/rag/config/llm/options": {
|
| 1151 |
+
"get": {
|
| 1152 |
+
"tags": ["v1", "admin"],
|
| 1153 |
+
"summary": "Get LLM provider options",
|
| 1154 |
+
"description": "Returns a tree-like mapping of available LLM providers and their corresponding models and base URLs for frontend dropdowns.",
|
| 1155 |
+
"operationId": "get_llm_provider_options_api_v1_admin_rag_config_llm_options_get",
|
| 1156 |
+
"responses": {
|
| 1157 |
+
"200": {
|
| 1158 |
+
"description": "Successful Response",
|
| 1159 |
+
"content": {
|
| 1160 |
+
"application/json": {
|
| 1161 |
+
"schema": {
|
| 1162 |
+
"type": "object",
|
| 1163 |
+
"title": "Response Get Llm Provider Options Api V1 Admin Rag Config Llm Options Get"
|
| 1164 |
+
}
|
| 1165 |
+
}
|
| 1166 |
+
}
|
| 1167 |
+
}
|
| 1168 |
+
},
|
| 1169 |
+
"security": [
|
| 1170 |
+
{
|
| 1171 |
+
"JWTBearer": []
|
| 1172 |
+
}
|
| 1173 |
+
]
|
| 1174 |
+
}
|
| 1175 |
+
},
|
| 1176 |
"/api/v1/admin/rag/config/vector_db": {
|
| 1177 |
"get": {
|
| 1178 |
"tags": ["v1", "admin"],
|
|
|
|
| 1200 |
"patch": {
|
| 1201 |
"tags": ["v1", "admin"],
|
| 1202 |
"summary": "Update VectorDB config",
|
| 1203 |
+
"description": "Partially update the VectorDB configuration. persist_path is read-only.",
|
| 1204 |
"operationId": "update_vector_db_config_api_v1_admin_rag_config_vector_db_patch",
|
| 1205 |
"requestBody": {
|
| 1206 |
"content": {
|
|
|
|
| 1241 |
]
|
| 1242 |
}
|
| 1243 |
},
|
| 1244 |
+
"/api/v1/admin/rag/config/vector_db/options": {
|
| 1245 |
+
"get": {
|
| 1246 |
+
"tags": ["v1", "admin"],
|
| 1247 |
+
"summary": "Get Vector DB options",
|
| 1248 |
+
"description": "Returns available embedding models for frontend dropdowns.",
|
| 1249 |
+
"operationId": "get_vector_db_options_api_v1_admin_rag_config_vector_db_options_get",
|
| 1250 |
+
"responses": {
|
| 1251 |
+
"200": {
|
| 1252 |
+
"description": "Successful Response",
|
| 1253 |
+
"content": {
|
| 1254 |
+
"application/json": {
|
| 1255 |
+
"schema": {
|
| 1256 |
+
"type": "object",
|
| 1257 |
+
"title": "Response Get Vector Db Options Api V1 Admin Rag Config Vector Db Options Get"
|
| 1258 |
+
}
|
| 1259 |
+
}
|
| 1260 |
+
}
|
| 1261 |
+
}
|
| 1262 |
+
},
|
| 1263 |
+
"security": [
|
| 1264 |
+
{
|
| 1265 |
+
"JWTBearer": []
|
| 1266 |
+
}
|
| 1267 |
+
]
|
| 1268 |
+
}
|
| 1269 |
+
},
|
| 1270 |
"/api/v1/admin/rag/config/retrieval": {
|
| 1271 |
"get": {
|
| 1272 |
"tags": ["v1", "admin"],
|
|
|
|
| 1335 |
]
|
| 1336 |
}
|
| 1337 |
},
|
| 1338 |
+
"/api/v1/admin/rag/config/retrieval/options": {
|
| 1339 |
+
"get": {
|
| 1340 |
+
"tags": ["v1", "admin"],
|
| 1341 |
+
"summary": "Get Retrieval options",
|
| 1342 |
+
"description": "Returns available reranker models for frontend dropdowns.",
|
| 1343 |
+
"operationId": "get_retrieval_options_api_v1_admin_rag_config_retrieval_options_get",
|
| 1344 |
+
"responses": {
|
| 1345 |
+
"200": {
|
| 1346 |
+
"description": "Successful Response",
|
| 1347 |
+
"content": {
|
| 1348 |
+
"application/json": {
|
| 1349 |
+
"schema": {
|
| 1350 |
+
"type": "object",
|
| 1351 |
+
"title": "Response Get Retrieval Options Api V1 Admin Rag Config Retrieval Options Get"
|
| 1352 |
+
}
|
| 1353 |
+
}
|
| 1354 |
+
}
|
| 1355 |
+
}
|
| 1356 |
+
},
|
| 1357 |
+
"security": [
|
| 1358 |
+
{
|
| 1359 |
+
"JWTBearer": []
|
| 1360 |
+
}
|
| 1361 |
+
]
|
| 1362 |
+
}
|
| 1363 |
+
},
|
| 1364 |
"/api/v1/auth/api-key/generate": {
|
| 1365 |
"post": {
|
| 1366 |
"tags": ["v1", "auth-iframe"],
|
|
|
|
| 1672 |
"required": ["message", "user"],
|
| 1673 |
"title": "ActivateAccountResponse"
|
| 1674 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1675 |
"ApiKeyLoginRequest": {
|
| 1676 |
"properties": {
|
| 1677 |
"api_key": {
|
|
|
|
| 1684 |
"title": "ApiKeyLoginRequest",
|
| 1685 |
"description": "POST /auth/login/api-key â body."
|
| 1686 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1687 |
"Body_upload_document_api_v1_documents_upload_post": {
|
| 1688 |
"properties": {
|
| 1689 |
"file": {
|
|
|
|
| 1751 |
"title": "ChatMessageResponse",
|
| 1752 |
"description": "Mirrors the message object in OpenAPI spec."
|
| 1753 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1754 |
"ChatRESTRequest": {
|
| 1755 |
"properties": {
|
| 1756 |
"content": {
|
|
|
|
| 2156 |
"reasoning_effort": {
|
| 2157 |
"type": "string",
|
| 2158 |
"title": "Reasoning Effort",
|
| 2159 |
+
"description": "Provider/model-specific reasoning effort. OpenAI reasoning models use values such as none, minimal, low, medium, high, or xhigh depending on the model. DeepSeek thinking mode uses high or max. Use null for models without reasoning effort."
|
| 2160 |
},
|
| 2161 |
"temperature": {
|
| 2162 |
"type": "number",
|
|
|
|
| 2285 |
"required": ["page", "limit", "total", "total_pages"],
|
| 2286 |
"title": "PaginationDTO"
|
| 2287 |
},
|
| 2288 |
+
"PatchUser": {
|
| 2289 |
+
"properties": {
|
| 2290 |
+
"email": {
|
| 2291 |
+
"type": "string",
|
| 2292 |
+
"format": "email",
|
| 2293 |
+
"title": "Email"
|
| 2294 |
+
},
|
| 2295 |
+
"name": {
|
| 2296 |
+
"type": "string",
|
| 2297 |
+
"title": "Name"
|
| 2298 |
+
},
|
| 2299 |
+
"phone": {
|
| 2300 |
+
"type": "integer",
|
| 2301 |
+
"title": "Phone"
|
| 2302 |
+
},
|
| 2303 |
+
"role": {
|
| 2304 |
+
"$ref": "#/components/schemas/app__schema__auth_schema__Role"
|
| 2305 |
+
},
|
| 2306 |
+
"is_active": {
|
| 2307 |
+
"type": "boolean",
|
| 2308 |
+
"title": "Is Active"
|
| 2309 |
+
},
|
| 2310 |
+
"password": {
|
| 2311 |
+
"type": "string",
|
| 2312 |
+
"title": "Password"
|
| 2313 |
+
}
|
| 2314 |
+
},
|
| 2315 |
+
"additionalProperties": false,
|
| 2316 |
+
"type": "object",
|
| 2317 |
+
"title": "PatchUser",
|
| 2318 |
+
"description": "PATCH user untuk admin: semua field opsional agar bisa edit parsial.\n\nDilarang mengubah: id, user_token."
|
| 2319 |
+
},
|
| 2320 |
"RAGConfigResponse": {
|
| 2321 |
"properties": {
|
| 2322 |
"path": {
|
|
|
|
| 2361 |
},
|
| 2362 |
"identity_number": {
|
| 2363 |
"type": "string",
|
| 2364 |
+
"pattern": "^[0-9]+$",
|
| 2365 |
"title": "Identity Number",
|
| 2366 |
+
"description": "Student ID (NIM) or Lecturer ID (NIP) â numbers only"
|
| 2367 |
}
|
| 2368 |
},
|
| 2369 |
"type": "object",
|
|
|
|
| 2584 |
"title": "UploadDocumentResponse",
|
| 2585 |
"description": "POST /documents/upload â 202 Accepted response.\nSource: openapi-spec.yml /documents/upload 202 schema"
|
| 2586 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2587 |
"UpsertWhitelist": {
|
| 2588 |
"properties": {
|
| 2589 |
"source_type": {
|
|
|
|
| 2614 |
"type": "string",
|
| 2615 |
"title": "Email"
|
| 2616 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2617 |
"name": {
|
| 2618 |
"type": "string",
|
| 2619 |
"title": "Name"
|
|
|
|
| 2650 |
"type": "string",
|
| 2651 |
"format": "date-time",
|
| 2652 |
"title": "Updated At"
|
| 2653 |
+
},
|
| 2654 |
+
"user_token": {
|
| 2655 |
+
"type": "string",
|
| 2656 |
+
"title": "User Token"
|
| 2657 |
}
|
| 2658 |
},
|
| 2659 |
"type": "object",
|
|
|
|
| 2802 |
},
|
| 2803 |
"app__schema__user_schema__Role": {
|
| 2804 |
"type": "string",
|
| 2805 |
+
"enum": ["admin", "student", "lecturer"],
|
| 2806 |
"title": "Role",
|
| 2807 |
"description": "An enumeration."
|
| 2808 |
}
|