mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-10-19 05:48:15 +00:00
feat(app): lint & beautify
This commit is contained in:
@@ -2,118 +2,117 @@ import { Dialog, DialogPanel } from '@headlessui/react';
|
|||||||
import { Loader2, Trash2 } from 'lucide-react';
|
import { Loader2, Trash2 } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import {
|
import { ConfigModelProvider } from '@/lib/config/types';
|
||||||
ConfigModelProvider,
|
|
||||||
} from '@/lib/config/types';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const DeleteProvider = ({
|
const DeleteProvider = ({
|
||||||
modelProvider,
|
modelProvider,
|
||||||
setProviders,
|
setProviders,
|
||||||
}: {
|
}: {
|
||||||
modelProvider: ConfigModelProvider;
|
modelProvider: ConfigModelProvider;
|
||||||
setProviders: React.Dispatch<React.SetStateAction<ConfigModelProvider[]>>;
|
setProviders: React.Dispatch<React.SetStateAction<ConfigModelProvider[]>>;
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleDelete = async (e: React.FormEvent) => {
|
const handleDelete = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/providers/${modelProvider.id}`, {
|
const res = await fetch(`/api/providers/${modelProvider.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to delete provider');
|
throw new Error('Failed to delete provider');
|
||||||
}
|
}
|
||||||
|
|
||||||
setProviders((prev) => {
|
setProviders((prev) => {
|
||||||
return prev.filter((p) => p.id !== modelProvider.id);
|
return prev.filter((p) => p.id !== modelProvider.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success('Provider deleted successfully.');
|
toast.success('Provider deleted successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting provider:', error);
|
console.error('Error deleting provider:', error);
|
||||||
toast.error('Failed to delete provider.');
|
toast.error('Failed to delete provider.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
className="group p-1.5 rounded-md hover:bg-light-200 hover:dark:bg-dark-200 transition-colors group"
|
className="group p-1.5 rounded-md hover:bg-light-200 hover:dark:bg-dark-200 transition-colors group"
|
||||||
title="Delete provider"
|
title="Delete provider"
|
||||||
|
>
|
||||||
|
<Trash2
|
||||||
|
size={14}
|
||||||
|
className="text-black/60 dark:text-white/60 group-hover:text-red-500 group-hover:dark:text-red-400"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<Dialog
|
||||||
|
static
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
className="relative z-[60]"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/30 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<Trash2
|
<DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg">
|
||||||
size={14}
|
<div className="px-6 pt-6 pb-4">
|
||||||
className="text-black/60 dark:text-white/60 group-hover:text-red-500 group-hover:dark:text-red-400"
|
<h3 className="text-black/90 dark:text-white/90 font-medium">
|
||||||
/>
|
Delete provider
|
||||||
</button>
|
</h3>
|
||||||
<AnimatePresence>
|
</div>
|
||||||
{open && (
|
<div className="border-t border-light-200 dark:border-dark-200" />
|
||||||
<Dialog
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||||
static
|
<p className="text-SM text-black/60 dark:text-white/60">
|
||||||
open={open}
|
Are you sure you want to delete the provider "
|
||||||
onClose={() => setOpen(false)}
|
{modelProvider.name}"? This action cannot be undone.
|
||||||
className="relative z-[60]"
|
</p>
|
||||||
>
|
</div>
|
||||||
<motion.div
|
<div className="px-6 py-6 flex justify-end space-x-2">
|
||||||
initial={{ opacity: 0 }}
|
<button
|
||||||
animate={{ opacity: 1 }}
|
disabled={loading}
|
||||||
exit={{ opacity: 0 }}
|
onClick={() => setOpen(false)}
|
||||||
transition={{ duration: 0.1 }}
|
className="px-4 py-2 rounded-lg text-sm border border-light-200 dark:border-dark-200 text-black dark:text-white bg-light-secondary/50 dark:bg-dark-secondary/50 hover:bg-light-secondary hover:dark:bg-dark-secondary hover:border-light-300 hover:dark:border-dark-300 flex flex-row items-center space-x-1 active:scale-95 transition duration-200"
|
||||||
className="fixed inset-0 flex w-screen items-center justify-center p-4 bg-black/30 backdrop-blur-sm"
|
>
|
||||||
>
|
Cancel
|
||||||
<DialogPanel className="w-full mx-4 lg:w-[600px] max-h-[85vh] flex flex-col border bg-light-primary dark:bg-dark-primary border-light-secondary dark:border-dark-secondary rounded-lg">
|
</button>
|
||||||
<div className="px-6 pt-6 pb-4">
|
<button
|
||||||
<h3 className="text-black/90 dark:text-white/90 font-medium">
|
disabled={loading}
|
||||||
Delete provider
|
onClick={handleDelete}
|
||||||
</h3>
|
className="px-4 py-2 rounded-lg text-sm bg-red-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200"
|
||||||
</div>
|
>
|
||||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
{loading ? (
|
||||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
<Loader2 className="animate-spin" size={16} />
|
||||||
<p className='text-SM text-black/60 dark:text-white/60'>
|
) : (
|
||||||
Are you sure you want to delete the provider "{modelProvider.name}"? This action cannot be undone.
|
'Delete'
|
||||||
</p>
|
)}
|
||||||
</div>
|
</button>
|
||||||
<div className="px-6 py-6 flex justify-end space-x-2">
|
</div>
|
||||||
<button
|
</DialogPanel>
|
||||||
disabled={loading}
|
</motion.div>
|
||||||
onClick={() => setOpen(false)}
|
</Dialog>
|
||||||
className="px-4 py-2 rounded-lg text-sm border border-light-200 dark:border-dark-200 text-black dark:text-white bg-light-secondary/50 dark:bg-dark-secondary/50 hover:bg-light-secondary hover:dark:bg-dark-secondary hover:border-light-300 hover:dark:border-dark-300 flex flex-row items-center space-x-1 active:scale-95 transition duration-200"
|
)}
|
||||||
>
|
</AnimatePresence>
|
||||||
Cancel
|
</>
|
||||||
</button>
|
);
|
||||||
<button
|
|
||||||
disabled={loading}
|
|
||||||
onClick={handleDelete}
|
|
||||||
className="px-4 py-2 rounded-lg text-sm bg-red-500 text-white font-medium disabled:opacity-85 hover:opacity-85 active:scale-95 transition duration-200"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<Loader2 className="animate-spin" size={16} />
|
|
||||||
) : (
|
|
||||||
'Delete'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</DialogPanel>
|
|
||||||
</motion.div>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteProvider;
|
export default DeleteProvider;
|
||||||
|
@@ -9,199 +9,209 @@ import UpdateProvider from './UpdateProviderDialog';
|
|||||||
import DeleteProvider from './DeleteProviderDialog';
|
import DeleteProvider from './DeleteProviderDialog';
|
||||||
|
|
||||||
const ModelProvider = ({
|
const ModelProvider = ({
|
||||||
modelProvider,
|
modelProvider,
|
||||||
setProviders,
|
setProviders,
|
||||||
fields,
|
fields,
|
||||||
}: {
|
}: {
|
||||||
modelProvider: ConfigModelProvider;
|
modelProvider: ConfigModelProvider;
|
||||||
fields: UIConfigField[];
|
fields: UIConfigField[];
|
||||||
setProviders: React.Dispatch<React.SetStateAction<ConfigModelProvider[]>>;
|
setProviders: React.Dispatch<React.SetStateAction<ConfigModelProvider[]>>;
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const handleModelDelete = async (
|
const handleModelDelete = async (
|
||||||
type: 'chat' | 'embedding',
|
type: 'chat' | 'embedding',
|
||||||
modelKey: string,
|
modelKey: string,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/providers/${modelProvider.id}/models`, {
|
const res = await fetch(`/api/providers/${modelProvider.id}/models`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ key: modelKey, type: type }),
|
body: JSON.stringify({ key: modelKey, type: type }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to delete model: ' + (await res.text()));
|
throw new Error('Failed to delete model: ' + (await res.text()));
|
||||||
|
}
|
||||||
|
|
||||||
|
setProviders(
|
||||||
|
(prev) =>
|
||||||
|
prev.map((provider) => {
|
||||||
|
if (provider.id === modelProvider.id) {
|
||||||
|
return {
|
||||||
|
...provider,
|
||||||
|
...(type === 'chat'
|
||||||
|
? {
|
||||||
|
chatModels: provider.chatModels.filter(
|
||||||
|
(m) => m.key !== modelKey,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
embeddingModels: provider.embeddingModels.filter(
|
||||||
|
(m) => m.key !== modelKey,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
return provider;
|
||||||
|
}) as ConfigModelProvider[],
|
||||||
|
);
|
||||||
|
|
||||||
setProviders(
|
toast.success('Model deleted successfully.');
|
||||||
(prev) =>
|
} catch (err) {
|
||||||
prev.map((provider) => {
|
console.error('Failed to delete model', err);
|
||||||
if (provider.id === modelProvider.id) {
|
toast.error('Failed to delete model.');
|
||||||
return {
|
}
|
||||||
...provider,
|
};
|
||||||
...(type === 'chat'
|
|
||||||
? {
|
|
||||||
chatModels: provider.chatModels.filter(
|
|
||||||
(m) => m.key !== modelKey,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
embeddingModels: provider.embeddingModels.filter(
|
|
||||||
(m) => m.key !== modelKey,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return provider;
|
|
||||||
}) as ConfigModelProvider[],
|
|
||||||
);
|
|
||||||
|
|
||||||
toast.success('Model deleted successfully.');
|
return (
|
||||||
} catch (err) {
|
<div
|
||||||
console.error('Failed to delete model', err);
|
key={modelProvider.id}
|
||||||
toast.error('Failed to delete model.');
|
className="border border-light-200 dark:border-dark-200 rounded-lg overflow-hidden"
|
||||||
}
|
>
|
||||||
};
|
<div
|
||||||
|
className={cn(
|
||||||
return (
|
'group px-5 py-4 flex flex-row justify-between w-full cursor-pointer hover:bg-light-secondary hover:dark:bg-dark-secondary transition duration-200 items-center',
|
||||||
<div
|
!open && 'rounded-lg',
|
||||||
key={modelProvider.id}
|
)}
|
||||||
className="border border-light-200 dark:border-dark-200 rounded-lg overflow-hidden"
|
onClick={() => setOpen(!open)}
|
||||||
>
|
>
|
||||||
<div
|
<p className="text-black dark:text-white font-medium">
|
||||||
className={cn(
|
{modelProvider.name}
|
||||||
'group px-5 py-4 flex flex-row justify-between w-full cursor-pointer hover:bg-light-secondary hover:dark:bg-dark-secondary transition duration-200 items-center',
|
</p>
|
||||||
!open && 'rounded-lg',
|
<div className="flex items-center gap-4">
|
||||||
)}
|
<div className="flex flex-row items-center">
|
||||||
onClick={() => setOpen(!open)}
|
<UpdateProvider
|
||||||
>
|
fields={fields}
|
||||||
<p className="text-black dark:text-white font-medium">
|
modelProvider={modelProvider}
|
||||||
{modelProvider.name}
|
setProviders={setProviders}
|
||||||
</p>
|
/>
|
||||||
<div className="flex items-center gap-4">
|
<DeleteProvider
|
||||||
<div className="flex flex-row items-center">
|
modelProvider={modelProvider}
|
||||||
<UpdateProvider
|
setProviders={setProviders}
|
||||||
fields={fields}
|
/>
|
||||||
modelProvider={modelProvider}
|
</div>
|
||||||
setProviders={setProviders}
|
<ChevronDown
|
||||||
/>
|
size={16}
|
||||||
<DeleteProvider
|
className={cn(
|
||||||
modelProvider={modelProvider}
|
open ? 'rotate-180' : '',
|
||||||
setProviders={setProviders}
|
'transition duration-200 text-black/70 dark:text-white/70 group-hover:text-sky-500',
|
||||||
/>
|
)}
|
||||||
</div>
|
/>
|
||||||
<ChevronDown
|
|
||||||
size={16}
|
|
||||||
className={cn(
|
|
||||||
open ? 'rotate-180' : '',
|
|
||||||
'transition duration-200 text-black/70 dark:text-white/70 group-hover:text-sky-500',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<AnimatePresence>
|
|
||||||
{open && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ height: 0, opacity: 0 }}
|
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
|
||||||
exit={{ height: 0, opacity: 0 }}
|
|
||||||
transition={{ duration: 0.1 }}
|
|
||||||
>
|
|
||||||
<div className="border-t border-light-200 dark:border-dark-200" />
|
|
||||||
<div className="flex flex-col gap-y-4 px-5 py-4">
|
|
||||||
{modelProvider.chatModels.length > 0 && (
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<div className="flex flex-row w-full justify-between items-center">
|
|
||||||
<p className="text-xs text-black/70 dark:text-white/70">
|
|
||||||
Chat models
|
|
||||||
</p>
|
|
||||||
<AddModel
|
|
||||||
providerId={modelProvider.id}
|
|
||||||
setProviders={setProviders}
|
|
||||||
type="chat"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{modelProvider.chatModels.some((m) => m.key === 'error') ? (
|
|
||||||
<div className="flex flex-row items-center gap-2 text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
|
||||||
<AlertCircle size={16} className="shrink-0" />
|
|
||||||
<span className="break-words">
|
|
||||||
{modelProvider.chatModels.find((m) => m.key === 'error')?.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-row flex-wrap gap-2">
|
|
||||||
{modelProvider.chatModels.map((model, index) => (
|
|
||||||
<div
|
|
||||||
key={`${modelProvider.id}-chat-${model.key}-${index}`}
|
|
||||||
className="flex flex-row items-center space-x-1 text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
|
|
||||||
>
|
|
||||||
<span>{model.name}</span>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
handleModelDelete('chat', model.key);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{modelProvider.embeddingModels.length > 0 && (
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<div className="flex flex-row w-full justify-between items-center">
|
|
||||||
<p className="text-xs text-black/70 dark:text-white/70">
|
|
||||||
Embedding models
|
|
||||||
</p>
|
|
||||||
<AddModel
|
|
||||||
providerId={modelProvider.id}
|
|
||||||
setProviders={setProviders}
|
|
||||||
type="embedding"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{modelProvider.embeddingModels.some((m) => m.key === 'error') ? (
|
|
||||||
<div className="flex flex-row items-center gap-2 text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
|
||||||
<AlertCircle size={16} className="shrink-0" />
|
|
||||||
<span className="break-words">
|
|
||||||
{modelProvider.embeddingModels.find((m) => m.key === 'error')?.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-row flex-wrap gap-2">
|
|
||||||
{modelProvider.embeddingModels.map((model, index) => (
|
|
||||||
<div
|
|
||||||
key={`${modelProvider.id}-embedding-${model.key}-${index}`}
|
|
||||||
className="flex flex-row items-center space-x-1 text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
|
|
||||||
>
|
|
||||||
<span>{model.name}</span>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
handleModelDelete('embedding', model.key);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
>
|
||||||
|
<div className="border-t border-light-200 dark:border-dark-200" />
|
||||||
|
<div className="flex flex-col gap-y-4 px-5 py-4">
|
||||||
|
{modelProvider.chatModels.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<div className="flex flex-row w-full justify-between items-center">
|
||||||
|
<p className="text-xs text-black/70 dark:text-white/70">
|
||||||
|
Chat models
|
||||||
|
</p>
|
||||||
|
<AddModel
|
||||||
|
providerId={modelProvider.id}
|
||||||
|
setProviders={setProviders}
|
||||||
|
type="chat"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{modelProvider.chatModels.some((m) => m.key === 'error') ? (
|
||||||
|
<div className="flex flex-row items-center gap-2 text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
||||||
|
<AlertCircle size={16} className="shrink-0" />
|
||||||
|
<span className="break-words">
|
||||||
|
{
|
||||||
|
modelProvider.chatModels.find(
|
||||||
|
(m) => m.key === 'error',
|
||||||
|
)?.name
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-row flex-wrap gap-2">
|
||||||
|
{modelProvider.chatModels.map((model, index) => (
|
||||||
|
<div
|
||||||
|
key={`${modelProvider.id}-chat-${model.key}-${index}`}
|
||||||
|
className="flex flex-row items-center space-x-1 text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
|
||||||
|
>
|
||||||
|
<span>{model.name}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
handleModelDelete('chat', model.key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{modelProvider.embeddingModels.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<div className="flex flex-row w-full justify-between items-center">
|
||||||
|
<p className="text-xs text-black/70 dark:text-white/70">
|
||||||
|
Embedding models
|
||||||
|
</p>
|
||||||
|
<AddModel
|
||||||
|
providerId={modelProvider.id}
|
||||||
|
setProviders={setProviders}
|
||||||
|
type="embedding"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{modelProvider.embeddingModels.some(
|
||||||
|
(m) => m.key === 'error',
|
||||||
|
) ? (
|
||||||
|
<div className="flex flex-row items-center gap-2 text-sm text-red-500 dark:text-red-400 rounded-lg bg-red-50 dark:bg-red-950/20 px-3 py-2 border border-red-200 dark:border-red-900/30">
|
||||||
|
<AlertCircle size={16} className="shrink-0" />
|
||||||
|
<span className="break-words">
|
||||||
|
{
|
||||||
|
modelProvider.embeddingModels.find(
|
||||||
|
(m) => m.key === 'error',
|
||||||
|
)?.name
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-row flex-wrap gap-2">
|
||||||
|
{modelProvider.embeddingModels.map((model, index) => (
|
||||||
|
<div
|
||||||
|
key={`${modelProvider.id}-embedding-${model.key}-${index}`}
|
||||||
|
className="flex flex-row items-center space-x-1 text-sm text-black/70 dark:text-white/70 rounded-lg bg-light-secondary dark:bg-dark-secondary px-3 py-1.5"
|
||||||
|
>
|
||||||
|
<span>{model.name}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
handleModelDelete('embedding', model.key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ModelProvider;
|
export default ModelProvider;
|
||||||
|
@@ -11,4 +11,5 @@ export const getConfiguredModelProviderById = (
|
|||||||
return getConfiguredModelProviders().find((p) => p.id === id) ?? undefined;
|
return getConfiguredModelProviders().find((p) => p.id === id) ?? undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSearxngURL = () => configManager.getConfig('search.searxngURL', '')
|
export const getSearxngURL = () =>
|
||||||
|
configManager.getConfig('search.searxngURL', '');
|
||||||
|
@@ -17,7 +17,6 @@ type StringUIConfigField = BaseUIConfigField & {
|
|||||||
|
|
||||||
type SelectUIConfigFieldOptions = {
|
type SelectUIConfigFieldOptions = {
|
||||||
name: string;
|
name: string;
|
||||||
key: string;
|
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,8 +55,8 @@ type Config = {
|
|||||||
};
|
};
|
||||||
modelProviders: ConfigModelProvider[];
|
modelProviders: ConfigModelProvider[];
|
||||||
search: {
|
search: {
|
||||||
[key: string]: any
|
[key: string]: any;
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type EnvMap = {
|
type EnvMap = {
|
||||||
@@ -76,7 +75,7 @@ type ModelProviderUISection = {
|
|||||||
type UIConfigSections = {
|
type UIConfigSections = {
|
||||||
general: UIConfigField[];
|
general: UIConfigField[];
|
||||||
modelProviders: ModelProviderUISection[];
|
modelProviders: ModelProviderUISection[];
|
||||||
search: UIConfigField[];
|
search: UIConfigField[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
@@ -84,6 +83,8 @@ export type {
|
|||||||
Config,
|
Config,
|
||||||
EnvMap,
|
EnvMap,
|
||||||
UIConfigSections,
|
UIConfigSections,
|
||||||
|
SelectUIConfigField,
|
||||||
|
StringUIConfigField,
|
||||||
ModelProviderUISection,
|
ModelProviderUISection,
|
||||||
ConfigModelProvider,
|
ConfigModelProvider,
|
||||||
};
|
};
|
||||||
|
@@ -5,7 +5,7 @@ import OllamaProvider from './ollama';
|
|||||||
|
|
||||||
export const providers: Record<string, ProviderConstructor<any>> = {
|
export const providers: Record<string, ProviderConstructor<any>> = {
|
||||||
openai: OpenAIProvider,
|
openai: OpenAIProvider,
|
||||||
ollama: OllamaProvider
|
ollama: OllamaProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getModelProvidersUIConfigSection =
|
export const getModelProvidersUIConfigSection =
|
||||||
|
@@ -7,129 +7,131 @@ import { UIConfigField } from '@/lib/config/types';
|
|||||||
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry';
|
||||||
|
|
||||||
interface OllamaConfig {
|
interface OllamaConfig {
|
||||||
baseURL: string;
|
baseURL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerConfigFields: UIConfigField[] = [
|
const providerConfigFields: UIConfigField[] = [
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'Base URL',
|
name: 'Base URL',
|
||||||
key: 'baseURL',
|
key: 'baseURL',
|
||||||
description: 'The base URL for the Ollama',
|
description: 'The base URL for the Ollama',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'Ollama Base URL',
|
placeholder: 'Ollama Base URL',
|
||||||
default: process.env.DOCKER ? 'http://host.docker.internal:11434' : 'http://localhost:11434',
|
default: process.env.DOCKER
|
||||||
env: 'OLLAMA_BASE_URL',
|
? 'http://host.docker.internal:11434'
|
||||||
scope: 'server',
|
: 'http://localhost:11434',
|
||||||
},
|
env: 'OLLAMA_BASE_URL',
|
||||||
|
scope: 'server',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
class OllamaProvider extends BaseModelProvider<OllamaConfig> {
|
class OllamaProvider extends BaseModelProvider<OllamaConfig> {
|
||||||
constructor(id: string, name: string, config: OllamaConfig) {
|
constructor(id: string, name: string, config: OllamaConfig) {
|
||||||
super(id, name, config);
|
super(id, name, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDefaultModels(): Promise<ModelList> {
|
async getDefaultModels(): Promise<ModelList> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${this.config.baseURL}/api/tags`, {
|
const res = await fetch(`${this.config.baseURL}/api/tags`, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-type': 'application/json'
|
'Content-type': 'application/json',
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const data = await res.json()
|
const data = await res.json();
|
||||||
|
|
||||||
const models: Model[] = data.models.map((m: any) => {
|
|
||||||
return {
|
|
||||||
name: m.name,
|
|
||||||
key: m.model
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
embedding: models,
|
|
||||||
chat: models,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof TypeError) {
|
|
||||||
throw new Error('Error connecting to Ollama API. Please ensure the base URL is correct and the Ollama server is running.');
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getModelList(): Promise<ModelList> {
|
|
||||||
const defaultModels = await this.getDefaultModels();
|
|
||||||
const configProvider = getConfiguredModelProviderById(this.id)!;
|
|
||||||
|
|
||||||
|
const models: Model[] = data.models.map((m: any) => {
|
||||||
return {
|
return {
|
||||||
embedding: [
|
name: m.name,
|
||||||
...defaultModels.embedding,
|
key: m.model,
|
||||||
...configProvider.embeddingModels,
|
|
||||||
],
|
|
||||||
chat: [...defaultModels.chat, ...configProvider.chatModels],
|
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
embedding: models,
|
||||||
|
chat: models,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof TypeError) {
|
||||||
|
throw new Error(
|
||||||
|
'Error connecting to Ollama API. Please ensure the base URL is correct and the Ollama server is running.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getModelList(): Promise<ModelList> {
|
||||||
|
const defaultModels = await this.getDefaultModels();
|
||||||
|
const configProvider = getConfiguredModelProviderById(this.id)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
embedding: [
|
||||||
|
...defaultModels.embedding,
|
||||||
|
...configProvider.embeddingModels,
|
||||||
|
],
|
||||||
|
chat: [...defaultModels.chat, ...configProvider.chatModels],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadChatModel(key: string): Promise<BaseChatModel> {
|
||||||
|
const modelList = await this.getModelList();
|
||||||
|
|
||||||
|
const exists = modelList.chat.find((m) => m.key === key);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
throw new Error(
|
||||||
|
'Error Loading Ollama Chat Model. Invalid Model Selected',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadChatModel(key: string): Promise<BaseChatModel> {
|
return new ChatOllama({
|
||||||
const modelList = await this.getModelList();
|
temperature: 0.7,
|
||||||
|
model: key,
|
||||||
|
baseUrl: this.config.baseURL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const exists = modelList.chat.find((m) => m.key === key);
|
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
||||||
|
const modelList = await this.getModelList();
|
||||||
|
const exists = modelList.embedding.find((m) => m.key === key);
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Error Loading Ollama Chat Model. Invalid Model Selected',
|
'Error Loading Ollama Embedding Model. Invalid Model Selected.',
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return new ChatOllama({
|
|
||||||
temperature: 0.7,
|
|
||||||
model: key,
|
|
||||||
baseUrl: this.config.baseURL,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadEmbeddingModel(key: string): Promise<Embeddings> {
|
return new OllamaEmbeddings({
|
||||||
const modelList = await this.getModelList();
|
model: key,
|
||||||
const exists = modelList.embedding.find((m) => m.key === key);
|
baseUrl: this.config.baseURL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!exists) {
|
static parseAndValidate(raw: any): OllamaConfig {
|
||||||
throw new Error(
|
if (!raw || typeof raw !== 'object')
|
||||||
'Error Loading Ollama Embedding Model. Invalid Model Selected.',
|
throw new Error('Invalid config provided. Expected object');
|
||||||
);
|
if (!raw.baseURL)
|
||||||
}
|
throw new Error('Invalid config provided. Base URL must be provided');
|
||||||
|
|
||||||
return new OllamaEmbeddings({
|
return {
|
||||||
model: key,
|
baseURL: String(raw.baseURL),
|
||||||
baseUrl: this.config.baseURL,
|
};
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static parseAndValidate(raw: any): OllamaConfig {
|
static getProviderConfigFields(): UIConfigField[] {
|
||||||
if (!raw || typeof raw !== 'object')
|
return providerConfigFields;
|
||||||
throw new Error('Invalid config provided. Expected object');
|
}
|
||||||
if (!raw.baseURL)
|
|
||||||
throw new Error(
|
|
||||||
'Invalid config provided. Base URL must be provided',
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
static getProviderMetadata(): ProviderMetadata {
|
||||||
baseURL: String(raw.baseURL),
|
return {
|
||||||
};
|
key: 'ollama',
|
||||||
}
|
name: 'Ollama',
|
||||||
|
};
|
||||||
static getProviderConfigFields(): UIConfigField[] {
|
}
|
||||||
return providerConfigFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProviderMetadata(): ProviderMetadata {
|
|
||||||
return {
|
|
||||||
key: 'ollama',
|
|
||||||
name: 'Ollama',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OllamaProvider;
|
export default OllamaProvider;
|
||||||
|
@@ -67,7 +67,6 @@ class ModelRegistry {
|
|||||||
chatModels: m.chat,
|
chatModels: m.chat,
|
||||||
embeddingModels: m.embedding,
|
embeddingModels: m.embedding,
|
||||||
});
|
});
|
||||||
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user