wepee commited on
Commit
a91bfdd
·
1 Parent(s): 0120d07

change: display llm config

Browse files
resources/js/components/admin/admin-llm-form.tsx CHANGED
@@ -51,8 +51,8 @@ function SearchableSelect({
51
  <div className="relative" ref={containerRef}>
52
  <button
53
  className={cn(
54
- 'border-input bg-background ring-offset-background flex h-9 w-full items-center justify-between rounded-md border px-3 py-2 text-sm',
55
- 'focus:ring-ring focus:outline-none focus:ring-2 focus:ring-offset-2',
56
  disabled && 'cursor-not-allowed opacity-50',
57
  )}
58
  disabled={disabled}
@@ -70,7 +70,7 @@ function SearchableSelect({
70
 
71
  {open && (
72
  <div
73
- className="bg-popover text-popover-foreground absolute z-50 mt-1 w-full rounded-md border shadow-md"
74
  onMouseDown={(e) => e.stopPropagation()}
75
  >
76
  <div className="border-b p-1">
@@ -91,8 +91,9 @@ function SearchableSelect({
91
  <button
92
  key={option}
93
  className={cn(
94
- 'hover:bg-accent hover:text-accent-foreground flex w-full items-center rounded-sm px-2 py-1.5 text-left text-sm',
95
- value === option && 'bg-accent text-accent-foreground',
 
96
  )}
97
  type="button"
98
  onClick={() => {
@@ -192,7 +193,7 @@ function ApiKeyInput({
192
  onChange={(e) => onChange(e.target.value)}
193
  />
194
  <button
195
- className="text-muted-foreground hover:text-foreground absolute right-3 top-1/2 -translate-y-1/2"
196
  type="button"
197
  onClick={() => setShow((v) => !v)}
198
  >
@@ -227,7 +228,9 @@ export function AdminLlmForm({ llm }: { llm?: LlmConfig | null }) {
227
  });
228
  const [isSubmitting, setIsSubmitting] = useState(false);
229
  const [error, setError] = useState<string | undefined>();
230
- const [selectedTemplate, setSelectedTemplate] = useState<string>(initialSystemPromptKey);
 
 
231
 
232
  function setField<K extends keyof LlmForm>(
233
  key: K,
@@ -266,13 +269,17 @@ export function AdminLlmForm({ llm }: { llm?: LlmConfig | null }) {
266
  };
267
  }
268
 
269
- if (formData.active_model) payload.active_model = formData.active_model;
 
270
  if (formData.api_key) payload.api_key = formData.api_key;
271
  payload.reasoning_effort = formData.reasoning_effort || null;
272
  if (formData.max_tokens)
273
  payload.max_tokens = parseInt(formData.max_tokens, 10);
274
  if (formData.request_timeout)
275
- payload.request_timeout = parseInt(formData.request_timeout, 10);
 
 
 
276
  if (formData.temperature)
277
  payload.temperature = parseFloat(formData.temperature);
278
  if (formData.system_prompt) {
@@ -285,13 +292,15 @@ export function AdminLlmForm({ llm }: { llm?: LlmConfig | null }) {
285
 
286
  const savedProviders = result.providers ?? {};
287
  const savedSystemPrompts = result.system_prompts ?? {};
288
- const savedKey = savedSystemPrompts[selectedTemplate] !== undefined
289
- ? selectedTemplate
290
- : (Object.keys(savedSystemPrompts)[0] ?? selectedTemplate);
 
291
 
292
  setSelectedTemplate(savedKey);
293
 
294
- const savedProviderInfo = savedProviders[result.active_provider ?? ''];
 
295
  setFormData({
296
  active_model: stringValue(result.active_model),
297
  active_provider: stringValue(result.active_provider),
@@ -322,145 +331,238 @@ export function AdminLlmForm({ llm }: { llm?: LlmConfig | null }) {
322
  : [];
323
 
324
  return (
325
- <Card className="border-(--lecturer-border) bg-(--lecturer-surface)">
326
- <CardContent className="pt-6">
327
- <form
328
- className="grid gap-4"
329
- onSubmit={(e) => {
330
- void handleSubmit(e);
331
- }}
332
- >
333
- <ConfigError message={error} />
334
-
335
- <div className="grid gap-3 md:grid-cols-2">
336
- <Field
337
- current={llm?.active_provider}
338
- hint="Penyedia layanan AI yang digunakan untuk menjawab pertanyaan."
339
- label="Provider"
340
- >
341
- <Select
342
- value={formData.active_provider}
343
- onValueChange={handleProviderChange}
 
344
  >
345
- <SelectTrigger className="w-full">
346
- <SelectValue placeholder="Pilih provider" />
347
- </SelectTrigger>
348
- <SelectContent>
349
- {Object.keys(providers).map((p) => (
350
- <SelectItem key={p} value={p}>
351
- {p.charAt(0).toUpperCase() + p.slice(1)}
352
- </SelectItem>
353
- ))}
354
- </SelectContent>
355
- </Select>
356
- </Field>
357
-
358
- <Field
359
- current={llm?.active_model}
360
- hint="Model AI yang digunakan. Pilih provider terlebih dahulu."
361
- label="Model"
362
- >
363
- <SearchableSelect
364
- disabled={!formData.active_provider}
365
- options={providerConfig?.model_options ?? []}
366
- placeholder={
367
- formData.active_provider
368
- ? 'Pilih model'
369
- : 'Pilih provider dulu'
370
- }
371
- searchPlaceholder="Cari model..."
372
- value={formData.active_model}
373
- onChange={(v) => setField('active_model', v)}
374
- />
375
- </Field>
376
 
377
- <div className="md:col-span-2">
378
  <Field
379
- hint="URL endpoint API provider. Pilih dari daftar atau masukkan custom URL."
380
- label="Base URL"
 
381
  >
382
- <CreatableBaseUrlSelect
383
- key={formData.active_provider}
384
- options={baseUrlOptions}
385
- value={formData.base_url}
386
- onChange={(v) => setField('base_url', v)}
 
 
 
 
 
 
 
 
 
 
387
  />
388
  </Field>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
- <Field
392
- hint="Tingkat kreativitas jawaban (0–1). Rendah = konsisten, tinggi = lebih variatif."
393
- label="Temperature"
394
- >
395
- <Input
396
- min="0"
397
- step="0.01"
398
- type="number"
399
- value={formData.temperature}
400
- onChange={(event) =>
401
- setField('temperature', event.target.value)
402
- }
403
- />
404
- </Field>
 
 
405
 
406
- <Field
407
- hint="Batas maksimum token yang dihasilkan per respons."
408
- label="Max Tokens"
409
- >
410
- <Input
411
- min="1"
412
- type="number"
413
- value={formData.max_tokens}
414
- onChange={(event) =>
415
- setField('max_tokens', event.target.value)
416
- }
417
- />
418
- </Field>
 
 
 
419
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  <Field
421
- hint="Batas waktu menunggu respons dari API dalam detik."
422
- label="Request Timeout"
423
  >
424
- <Input
425
- min="1"
426
- type="number"
427
- value={formData.request_timeout}
428
- onChange={(event) =>
429
- setField(
430
- 'request_timeout',
431
- event.target.value,
432
- )
433
- }
434
  />
435
  </Field>
436
-
 
 
 
 
 
 
 
 
 
 
437
  <Field
438
- hint="Tingkat usaha reasoning model. Tersedia sesuai provider yang dipilih."
439
- label="Reasoning Effort"
440
  >
441
  <Select
442
- disabled={!formData.active_provider}
443
- value={formData.reasoning_effort || '__none__'}
444
- onValueChange={(val) =>
 
445
  setField(
446
- 'reasoning_effort',
447
- val === '__none__' ? '' : val,
448
- )
449
- }
450
  >
451
  <SelectTrigger className="w-full">
452
- <SelectValue placeholder="Tidak diatur" />
453
  </SelectTrigger>
454
  <SelectContent>
455
- <SelectItem value="__none__">
456
- Tidak diatur
457
- </SelectItem>
458
- {reasoningEffortOptions.map((effort) => (
459
- <SelectItem
460
- key={effort}
461
- value={effort}
462
- >
463
- {effort}
464
  </SelectItem>
465
  ))}
466
  </SelectContent>
@@ -468,66 +570,34 @@ export function AdminLlmForm({ llm }: { llm?: LlmConfig | null }) {
468
  </Field>
469
 
470
  <Field
471
- hint="API key untuk autentikasi ke provider. Nilai disimpan terenkripsi."
472
- label="API Key"
473
  >
474
- <ApiKeyInput
475
- value={formData.api_key}
476
- onChange={(v) => setField('api_key', v)}
 
 
 
 
 
 
477
  />
478
  </Field>
479
  </div>
480
-
481
- <Field
482
- hint="Pilih key system prompt yang ingin diedit. Setiap key menyimpan instruksi berbeda."
483
- label="Template System Prompt"
484
- >
485
- <Select
486
- disabled={systemPromptKeys.length === 0}
487
- value={selectedTemplate}
488
- onValueChange={(key) => {
489
- setSelectedTemplate(key);
490
- setField('system_prompt', systemPromptsMap[key] ?? '');
491
- }}
492
- >
493
- <SelectTrigger className="w-full">
494
- <SelectValue placeholder="Pilih key..." />
495
- </SelectTrigger>
496
- <SelectContent>
497
- {systemPromptKeys.map((key) => (
498
- <SelectItem key={key} value={key}>
499
- {key}
500
- </SelectItem>
501
- ))}
502
- </SelectContent>
503
- </Select>
504
- </Field>
505
-
506
- <Field
507
- hint="Instruksi awal yang selalu dikirim ke model sebelum percakapan dimulai."
508
- label="System Prompt"
509
- >
510
- <textarea
511
- className="border-input ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring min-h-36 w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
512
- value={formData.system_prompt}
513
- onChange={(event) => {
514
- setField('system_prompt', event.target.value);
515
- }}
516
- />
517
- </Field>
518
-
519
- <div className="flex justify-end">
520
- <Button disabled={isSubmitting} type="submit">
521
- {isSubmitting ? (
522
- <Spinner className="size-4" />
523
- ) : (
524
- <Save className="size-4" />
525
- )}
526
- Simpan LLM
527
- </Button>
528
- </div>
529
- </form>
530
- </CardContent>
531
- </Card>
532
  );
533
  }
 
51
  <div className="relative" ref={containerRef}>
52
  <button
53
  className={cn(
54
+ 'flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background',
55
+ 'focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-none',
56
  disabled && 'cursor-not-allowed opacity-50',
57
  )}
58
  disabled={disabled}
 
70
 
71
  {open && (
72
  <div
73
+ className="absolute z-50 mt-1 w-full rounded-md border bg-popover text-popover-foreground shadow-md"
74
  onMouseDown={(e) => e.stopPropagation()}
75
  >
76
  <div className="border-b p-1">
 
91
  <button
92
  key={option}
93
  className={cn(
94
+ 'flex w-full items-center rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground',
95
+ value === option &&
96
+ 'bg-accent text-accent-foreground',
97
  )}
98
  type="button"
99
  onClick={() => {
 
193
  onChange={(e) => onChange(e.target.value)}
194
  />
195
  <button
196
+ className="absolute top-1/2 right-3 -translate-y-1/2 text-muted-foreground hover:text-foreground"
197
  type="button"
198
  onClick={() => setShow((v) => !v)}
199
  >
 
228
  });
229
  const [isSubmitting, setIsSubmitting] = useState(false);
230
  const [error, setError] = useState<string | undefined>();
231
+ const [selectedTemplate, setSelectedTemplate] = useState<string>(
232
+ initialSystemPromptKey,
233
+ );
234
 
235
  function setField<K extends keyof LlmForm>(
236
  key: K,
 
269
  };
270
  }
271
 
272
+ if (formData.active_model)
273
+ payload.active_model = formData.active_model;
274
  if (formData.api_key) payload.api_key = formData.api_key;
275
  payload.reasoning_effort = formData.reasoning_effort || null;
276
  if (formData.max_tokens)
277
  payload.max_tokens = parseInt(formData.max_tokens, 10);
278
  if (formData.request_timeout)
279
+ payload.request_timeout = parseInt(
280
+ formData.request_timeout,
281
+ 10,
282
+ );
283
  if (formData.temperature)
284
  payload.temperature = parseFloat(formData.temperature);
285
  if (formData.system_prompt) {
 
292
 
293
  const savedProviders = result.providers ?? {};
294
  const savedSystemPrompts = result.system_prompts ?? {};
295
+ const savedKey =
296
+ savedSystemPrompts[selectedTemplate] !== undefined
297
+ ? selectedTemplate
298
+ : (Object.keys(savedSystemPrompts)[0] ?? selectedTemplate);
299
 
300
  setSelectedTemplate(savedKey);
301
 
302
+ const savedProviderInfo =
303
+ savedProviders[result.active_provider ?? ''];
304
  setFormData({
305
  active_model: stringValue(result.active_model),
306
  active_provider: stringValue(result.active_provider),
 
331
  : [];
332
 
333
  return (
334
+ <form
335
+ className="grid gap-4"
336
+ onSubmit={(e) => {
337
+ void handleSubmit(e);
338
+ }}
339
+ >
340
+ <ConfigError message={error} />
341
+
342
+ {/* Model */}
343
+ <Card className="border-(--lecturer-border) bg-(--lecturer-surface)">
344
+ <CardContent className="px-6">
345
+ <div className="grid gap-3">
346
+ <h3 className="text-base font-semibold text-(--lecturer-text)">
347
+ Model
348
+ </h3>
349
+ <div className="grid gap-3 md:grid-cols-2">
350
+ <Field
351
+ current={llm?.active_provider}
352
+ hint="Penyedia layanan AI yang digunakan untuk menjawab pertanyaan."
353
+ label="Provider"
354
  >
355
+ <Select
356
+ value={formData.active_provider}
357
+ onValueChange={handleProviderChange}
358
+ >
359
+ <SelectTrigger className="w-full">
360
+ <SelectValue placeholder="Pilih provider" />
361
+ </SelectTrigger>
362
+ <SelectContent>
363
+ {Object.keys(providers).map((p) => (
364
+ <SelectItem key={p} value={p}>
365
+ {p.charAt(0).toUpperCase() +
366
+ p.slice(1)}
367
+ </SelectItem>
368
+ ))}
369
+ </SelectContent>
370
+ </Select>
371
+ </Field>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
 
373
  <Field
374
+ current={llm?.active_model}
375
+ hint="Model AI yang digunakan. Pilih provider terlebih dahulu."
376
+ label="Model"
377
  >
378
+ <SearchableSelect
379
+ disabled={!formData.active_provider}
380
+ options={
381
+ providerConfig?.model_options ?? []
382
+ }
383
+ placeholder={
384
+ formData.active_provider
385
+ ? 'Pilih model'
386
+ : 'Pilih provider dulu'
387
+ }
388
+ searchPlaceholder="Cari model..."
389
+ value={formData.active_model}
390
+ onChange={(v) =>
391
+ setField('active_model', v)
392
+ }
393
  />
394
  </Field>
395
+
396
+ <div className="md:col-span-2">
397
+ <Field
398
+ hint="URL endpoint API provider. Pilih dari daftar atau masukkan custom URL."
399
+ label="Base URL"
400
+ >
401
+ <CreatableBaseUrlSelect
402
+ key={formData.active_provider}
403
+ options={baseUrlOptions}
404
+ value={formData.base_url}
405
+ onChange={(v) =>
406
+ setField('base_url', v)
407
+ }
408
+ />
409
+ </Field>
410
+ </div>
411
  </div>
412
+ </div>
413
+ </CardContent>
414
+ </Card>
415
+
416
+ {/* Parameter */}
417
+ <Card className="border-(--lecturer-border) bg-(--lecturer-surface)">
418
+ <CardContent className="px-6">
419
+ <div className="grid gap-3">
420
+ <h3 className="text-base font-semibold text-(--lecturer-text)">
421
+ Parameter
422
+ </h3>
423
+ <div className="grid gap-3 md:grid-cols-2">
424
+ <Field
425
+ hint="Tingkat kreativitas jawaban (0–1). Rendah = konsisten, tinggi = lebih variatif."
426
+ label="Temperature"
427
+ >
428
+ <Input
429
+ min="0"
430
+ step="0.01"
431
+ type="number"
432
+ value={formData.temperature}
433
+ onChange={(event) =>
434
+ setField(
435
+ 'temperature',
436
+ event.target.value,
437
+ )
438
+ }
439
+ />
440
+ </Field>
441
 
442
+ <Field
443
+ hint="Batas maksimum token yang dihasilkan per respons."
444
+ label="Max Tokens"
445
+ >
446
+ <Input
447
+ min="1"
448
+ type="number"
449
+ value={formData.max_tokens}
450
+ onChange={(event) =>
451
+ setField(
452
+ 'max_tokens',
453
+ event.target.value,
454
+ )
455
+ }
456
+ />
457
+ </Field>
458
 
459
+ <Field
460
+ hint="Batas waktu menunggu respons dari API dalam detik."
461
+ label="Request Timeout"
462
+ >
463
+ <Input
464
+ min="1"
465
+ type="number"
466
+ value={formData.request_timeout}
467
+ onChange={(event) =>
468
+ setField(
469
+ 'request_timeout',
470
+ event.target.value,
471
+ )
472
+ }
473
+ />
474
+ </Field>
475
 
476
+ <Field
477
+ hint="Tingkat usaha reasoning model. Tersedia sesuai provider yang dipilih."
478
+ label="Reasoning Effort"
479
+ >
480
+ <Select
481
+ disabled={!formData.active_provider}
482
+ value={
483
+ formData.reasoning_effort || '__none__'
484
+ }
485
+ onValueChange={(val) =>
486
+ setField(
487
+ 'reasoning_effort',
488
+ val === '__none__' ? '' : val,
489
+ )
490
+ }
491
+ >
492
+ <SelectTrigger className="w-full">
493
+ <SelectValue placeholder="Tidak diatur" />
494
+ </SelectTrigger>
495
+ <SelectContent>
496
+ <SelectItem value="__none__">
497
+ Tidak diatur
498
+ </SelectItem>
499
+ {reasoningEffortOptions.map(
500
+ (effort) => (
501
+ <SelectItem
502
+ key={effort}
503
+ value={effort}
504
+ >
505
+ {effort}
506
+ </SelectItem>
507
+ ),
508
+ )}
509
+ </SelectContent>
510
+ </Select>
511
+ </Field>
512
+ </div>
513
+ </div>
514
+ </CardContent>
515
+ </Card>
516
+
517
+ {/* Autentikasi */}
518
+ <Card className="border-(--lecturer-border) bg-(--lecturer-surface)">
519
+ <CardContent className="px-6">
520
+ <div className="grid gap-3">
521
+ <h3 className="text-base font-semibold text-(--lecturer-text)">
522
+ Autentikasi
523
+ </h3>
524
  <Field
525
+ hint="API key untuk autentikasi ke provider. Nilai disimpan terenkripsi."
526
+ label="API Key"
527
  >
528
+ <ApiKeyInput
529
+ value={formData.api_key}
530
+ onChange={(v) => setField('api_key', v)}
 
 
 
 
 
 
 
531
  />
532
  </Field>
533
+ </div>
534
+ </CardContent>
535
+ </Card>
536
+
537
+ {/* System Prompt */}
538
+ <Card className="border-(--lecturer-border) bg-(--lecturer-surface)">
539
+ <CardContent className="px-6">
540
+ <div className="grid gap-3">
541
+ <h3 className="text-base font-semibold text-(--lecturer-text)">
542
+ System Prompt
543
+ </h3>
544
  <Field
545
+ hint="Pilih key system prompt yang ingin diedit. Setiap key menyimpan instruksi berbeda."
546
+ label="Template"
547
  >
548
  <Select
549
+ disabled={systemPromptKeys.length === 0}
550
+ value={selectedTemplate}
551
+ onValueChange={(key) => {
552
+ setSelectedTemplate(key);
553
  setField(
554
+ 'system_prompt',
555
+ systemPromptsMap[key] ?? '',
556
+ );
557
+ }}
558
  >
559
  <SelectTrigger className="w-full">
560
+ <SelectValue placeholder="Pilih key..." />
561
  </SelectTrigger>
562
  <SelectContent>
563
+ {systemPromptKeys.map((key) => (
564
+ <SelectItem key={key} value={key}>
565
+ {key}
 
 
 
 
 
 
566
  </SelectItem>
567
  ))}
568
  </SelectContent>
 
570
  </Field>
571
 
572
  <Field
573
+ hint="Instruksi awal yang selalu dikirim ke model sebelum percakapan dimulai."
574
+ label="Prompt"
575
  >
576
+ <textarea
577
+ className="min-h-36 w-full rounded-md border border-input px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none"
578
+ value={formData.system_prompt}
579
+ onChange={(event) => {
580
+ setField(
581
+ 'system_prompt',
582
+ event.target.value,
583
+ );
584
+ }}
585
  />
586
  </Field>
587
  </div>
588
+ </CardContent>
589
+ </Card>
590
+
591
+ <div className="flex justify-end">
592
+ <Button disabled={isSubmitting} type="submit">
593
+ {isSubmitting ? (
594
+ <Spinner className="size-4" />
595
+ ) : (
596
+ <Save className="size-4" />
597
+ )}
598
+ Simpan LLM
599
+ </Button>
600
+ </div>
601
+ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  );
603
  }