wepee commited on
Commit
49c6642
·
1 Parent(s): f98535e

fix: qa report all

Browse files
.env.example CHANGED
@@ -1,4 +1,4 @@
1
- APP_NAME=Laravel
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-[559px] 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,7 +570,7 @@ @layer components {
570
  }
571
 
572
  .register-error {
573
- @apply text-sm leading-snug font-medium;
574
  color: var(--register-danger);
575
  }
576
 
@@ -590,7 +590,7 @@ @layer components {
590
  }
591
 
592
  .login-card {
593
- @apply max-w-[561px];
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 = PROVIDER_CONFIG[provider];
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 = PROVIDER_CONFIG[formData.provider];
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
- <SelectItem value="openai">OpenAI</SelectItem>
390
- <SelectItem value="deepseek">Deepseek</SelectItem>
 
 
 
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 = buildUserPayload(formData);
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 = buildUserPayload(createFormData);
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 = buildUserPayload(editFormData);
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 buildUserPayload(form: UserForm): Record<string, unknown> {
 
 
 
 
 
 
 
 
 
 
 
 
 
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.password) payload.password = form.password;
534
- if (form.identity_number) payload.identity_number = form.identity_number;
535
  if (form.phone) payload.phone = form.phone;
536
- if (form.user_token) payload.user_token = form.user_token;
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
- return ragJson('/user', 'POST', payload);
 
 
 
 
 
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
- getCourse(courseId)
188
- .then((c) => setCourse(c))
189
- .catch((e: Error) => setBackendMessage(e.message));
190
-
191
- listCourseDocuments(courseId)
192
- .then((r) => setDocuments(r.data))
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 tidak
584
- dipakai.
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="Welcome" />
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="/favicon.ico" sizes="any">
35
- <link rel="icon" href="/favicon.svg" type="image/svg+xml">
36
- <link rel="apple-touch-icon" href="/apple-touch-icon.png">
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
- "200": {
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/UpsertUser"
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 YAML file.",
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. Only provided fields will be changed.",
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": "Reasoning effort for supported models. Use null for models without reasoning effort."
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), Lecturer ID (NIP), or uppercase code followed by digits"
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": ["student", "lecturer", "admin"],
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
  }