c3bf08ac8d
K8s Workspace Integration Tests / k8s-workspace-tests (push) Has been cancelled
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Web UI / check (push) Has been cancelled
247 lines
7.1 KiB
TypeScript
247 lines
7.1 KiB
TypeScript
import { CircleAlert, Info, Loader2, PlusCircle } from 'lucide-react';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
|
|
import type { EmbeddingModelCard, EmbeddingModelConfig } from '@/api';
|
|
import { DimensionSelect } from '@/components/select/DimensionSelect';
|
|
import { EmbeddingSelect } from '@/components/select/EmbeddingSelect';
|
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert.tsx';
|
|
import { Button } from '@/components/ui/button.tsx';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog.tsx';
|
|
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field.tsx';
|
|
import { Input } from '@/components/ui/input.tsx';
|
|
import { Textarea } from '@/components/ui/textarea.tsx';
|
|
import { useKbEmbeddingModels } from '@/hooks/useKbEmbeddingModels';
|
|
import { useKnowledgeBases } from '@/hooks/useKnowledgeBases';
|
|
import { useTranslation } from '@/i18n/useI18n.ts';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onCreated?: (knowledgeBaseId: string) => void;
|
|
onAddCredential?: () => void;
|
|
/**
|
|
* Bumped externally (e.g. after a credential is created) to ask the
|
|
* embedding selector to refetch its options.
|
|
*/
|
|
credentialRefetchTrigger?: number;
|
|
}
|
|
|
|
interface SelectedEmbedding {
|
|
type: string;
|
|
credentialId: string;
|
|
model: string;
|
|
card: EmbeddingModelCard;
|
|
}
|
|
|
|
export function CreateKnowledgeBaseDialog({
|
|
open,
|
|
onOpenChange,
|
|
onCreated,
|
|
onAddCredential,
|
|
credentialRefetchTrigger,
|
|
}: Props) {
|
|
const { t } = useTranslation();
|
|
const { create } = useKnowledgeBases();
|
|
const { providers, policy, loading } = useKbEmbeddingModels(credentialRefetchTrigger);
|
|
|
|
const [name, setName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [selected, setSelected] = useState<SelectedEmbedding | null>(null);
|
|
const [dimension, setDimension] = useState<number | null>(null);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [errorKey, setErrorKey] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setName('');
|
|
setDescription('');
|
|
setSelected(null);
|
|
setDimension(null);
|
|
setErrorKey(null);
|
|
setSubmitting(false);
|
|
}
|
|
}, [open]);
|
|
|
|
const dimensionOptions = useMemo<number[] | null>(() => {
|
|
if (!selected) return null;
|
|
const sd = selected.card.supported_dimensions;
|
|
return sd && sd.length > 0 ? sd : [selected.card.dimensions];
|
|
}, [selected]);
|
|
|
|
const handleSelectEmbedding = (sel: SelectedEmbedding) => {
|
|
setSelected(sel);
|
|
const sd = sel.card.supported_dimensions;
|
|
const defaultDim =
|
|
sd && sd.length > 0
|
|
? sd.includes(sel.card.dimensions)
|
|
? sel.card.dimensions
|
|
: sd[0]
|
|
: sel.card.dimensions;
|
|
setDimension(defaultDim);
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!name.trim()) {
|
|
setErrorKey('dialog-knowledge-base-create.errors.nameRequired');
|
|
return;
|
|
}
|
|
if (!selected) {
|
|
setErrorKey('dialog-knowledge-base-create.errors.embeddingRequired');
|
|
return;
|
|
}
|
|
if (dimension == null || dimension <= 0) {
|
|
setErrorKey('dialog-knowledge-base-create.errors.dimensionRequired');
|
|
return;
|
|
}
|
|
setErrorKey(null);
|
|
setSubmitting(true);
|
|
try {
|
|
const config: EmbeddingModelConfig = {
|
|
type: selected.type,
|
|
credential_id: selected.credentialId,
|
|
model: selected.model,
|
|
dimensions: dimension,
|
|
parameters: {},
|
|
};
|
|
const knowledgeBaseId = await create({
|
|
name: name.trim(),
|
|
description: description.trim(),
|
|
embedding_model_config: config,
|
|
});
|
|
onCreated?.(knowledgeBaseId);
|
|
onOpenChange(false);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const isLockedPolicy = policy && policy.kind !== 'any';
|
|
const noCompatibleModels = !loading && providers.length === 0;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="!w-[500px] !max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle>{t('dialog-knowledge-base-create.title')}</DialogTitle>
|
|
<DialogDescription>
|
|
{t('dialog-knowledge-base-create.description')}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{isLockedPolicy && policy?.dimension != null && (
|
|
<Alert>
|
|
<Info className="size-4" />
|
|
<AlertTitle>
|
|
{t('dialog-knowledge-base-create.policy.lockedTitle', {
|
|
dimension: policy.dimension,
|
|
})}
|
|
</AlertTitle>
|
|
<AlertDescription>
|
|
{t('dialog-knowledge-base-create.policy.lockedDescription', {
|
|
dimension: policy.dimension,
|
|
})}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{noCompatibleModels && (
|
|
<Alert variant="destructive">
|
|
<CircleAlert className="size-4" />
|
|
<AlertTitle>
|
|
{t('dialog-knowledge-base-create.policy.noCompatibleTitle')}
|
|
</AlertTitle>
|
|
<AlertDescription>
|
|
{isLockedPolicy && policy?.dimension != null
|
|
? t('dialog-knowledge-base-create.policy.noCompatibleLocked', {
|
|
dimension: policy.dimension,
|
|
})
|
|
: t('dialog-knowledge-base-create.policy.noCompatibleAny')}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<FieldGroup>
|
|
<Field>
|
|
<FieldLabel>{t('dialog-knowledge-base-create.name.label')}</FieldLabel>
|
|
<Input
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder={t('dialog-knowledge-base-create.name.placeholder')}
|
|
disabled={submitting}
|
|
/>
|
|
</Field>
|
|
<Field>
|
|
<FieldLabel>
|
|
{t('dialog-knowledge-base-create.descriptionField.label')}
|
|
</FieldLabel>
|
|
<Textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder={t(
|
|
'dialog-knowledge-base-create.descriptionField.placeholder',
|
|
)}
|
|
disabled={submitting}
|
|
rows={3}
|
|
/>
|
|
</Field>
|
|
<Field orientation="horizontal">
|
|
<FieldLabel>
|
|
{t('dialog-knowledge-base-create.embeddingModel.label')}
|
|
</FieldLabel>
|
|
<EmbeddingSelect
|
|
value={
|
|
selected
|
|
? {
|
|
type: selected.type,
|
|
credential_id: selected.credentialId,
|
|
model: selected.model,
|
|
}
|
|
: null
|
|
}
|
|
providers={providers}
|
|
loading={loading}
|
|
onChange={handleSelectEmbedding}
|
|
onAddCredential={onAddCredential}
|
|
/>
|
|
</Field>
|
|
<Field orientation="horizontal">
|
|
<FieldLabel>{t('dialog-knowledge-base-create.dimension.label')}</FieldLabel>
|
|
<DimensionSelect
|
|
value={dimension}
|
|
options={dimensionOptions}
|
|
onChange={setDimension}
|
|
disabled={submitting}
|
|
/>
|
|
</Field>
|
|
{errorKey && <p className="text-destructive text-sm">{t(errorKey)}</p>}
|
|
</FieldGroup>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => onOpenChange(false)}
|
|
disabled={submitting}
|
|
>
|
|
<CircleAlert className="size-3.5" />
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button onClick={handleSubmit} disabled={submitting || noCompatibleModels}>
|
|
{submitting ? (
|
|
<Loader2 className="size-3.5 animate-spin" />
|
|
) : (
|
|
<PlusCircle className="size-3.5" />
|
|
)}
|
|
{submitting ? t('common.creating') : t('common.create')}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|