Skip to content

Commit

Permalink
feat(frontend): support edit chat engine
Browse files Browse the repository at this point in the history
  • Loading branch information
634750802 committed Jul 24, 2024
1 parent 92711d9 commit 80c7e19
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 12 deletions.
32 changes: 24 additions & 8 deletions frontend/app/src/api/chat-engines.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BASE_URL, buildUrlParams, handleResponse, opaqueCookieHeader, type Page, type PageParams, zodPage } from '@/lib/request';
import { BASE_URL, buildUrlParams, handleErrors, handleResponse, opaqueCookieHeader, type Page, type PageParams, zodPage } from '@/lib/request';
import { zodJsonDate } from '@/lib/zod';
import { z, type ZodType } from 'zod';

Expand All @@ -9,6 +9,8 @@ export interface ChatEngine {
created_at: Date;
deleted_at: Date | null;
engine_options: ChatEngineOptions;
llm_id: number | null;
fast_llm_id: number | null;
is_default: boolean;
}

Expand Down Expand Up @@ -43,13 +45,13 @@ const kgOptionsSchema = z.object({

const llmOptionsSchema =
z.object({
condense_question_prompt: z.string(),
text_qa_prompt: z.string(),
refine_prompt: z.string(),
provider: z.string(),
reranker_provider: z.string(),
reranker_top_k: z.number(),
}).passthrough() as ZodType<ChatEngineLLMOptions, any, any>;
condense_question_prompt: z.string(),
text_qa_prompt: z.string(),
refine_prompt: z.string(),
provider: z.string(),
reranker_provider: z.string(),
reranker_top_k: z.number(),
}).passthrough() as ZodType<ChatEngineLLMOptions, any, any>;

const chatEngineOptionsSchema = z.object({
knowledge_graph: kgOptionsSchema,
Expand All @@ -63,6 +65,8 @@ const chatEngineSchema = z.object({
created_at: zodJsonDate(),
deleted_at: zodJsonDate().nullable(),
engine_options: chatEngineOptionsSchema,
llm_id: z.number().nullable(),
fast_llm_id: z.number().nullable(),
is_default: z.boolean(),
}) satisfies ZodType<ChatEngine, any, any>;

Expand All @@ -79,3 +83,15 @@ export async function getChatEngine (id: number): Promise<ChatEngine> {
})
.then(handleResponse(chatEngineSchema));
}

export async function updateChatEngine (id: number, partial: Partial<Pick<ChatEngine, 'name' | 'llm_id' | 'fast_llm_id' | 'engine_options' | 'is_default'>>): Promise<void> {
await fetch(BASE_URL + `/api/v1/admin/chat-engines/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...await opaqueCookieHeader(),
},
body: JSON.stringify(partial),
})
.then(handleErrors);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { ChatEngine } from '@/api/chat-engines';
import { ChatEngineOptionsDetails } from '@/components/chat-engine/chat-engine-options-details';
import { EditIsDefaultForm } from '@/components/chat-engine/edit-is-default-form';
import { EditLlmForm } from '@/components/chat-engine/edit-llm-form';
import { EditNameForm } from '@/components/chat-engine/edit-name-form';
import { LlmInfo } from '@/components/llm/LlmInfo';
import { OptionDetail } from '@/components/option-detail';
import { Separator } from '@/components/ui/separator';
import { format } from 'date-fns';
Expand All @@ -9,9 +13,12 @@ export function ChatEngineDetails ({ chatEngine }: { chatEngine: ChatEngine }) {
<div className="space-y-4 text-sm">
<div className="space-y-2 text-sm">
<OptionDetail title="ID" value={chatEngine.id} />
<OptionDetail title="Name" value={chatEngine.name} editPanel={<EditNameForm chatEngine={chatEngine} />} />
<OptionDetail title="LLM" value={<LlmInfo id={chatEngine.llm_id} />} editPanel={<EditLlmForm chatEngine={chatEngine} type="llm" />} />
<OptionDetail title="Fast LLM" value={<LlmInfo id={chatEngine.fast_llm_id} />} editPanel={<EditLlmForm chatEngine={chatEngine} type="fast_llm" />} />
<OptionDetail title="Created at" value={format(chatEngine.created_at, 'yyyy-MM-dd HH:mm:ss')} />
<OptionDetail title="Updated at" value={format(chatEngine.updated_at, 'yyyy-MM-dd HH:mm:ss')} />
<OptionDetail title="Is default" value={chatEngine.is_default ? 'Yes' : 'No'} />
<OptionDetail title="Is default" value={chatEngine.is_default ? 'Yes' : 'No'} editPanel={<EditIsDefaultForm chatEngine={chatEngine} />} />
</div>
<Separator />
<ChatEngineOptionsDetails options={chatEngine.engine_options} />
Expand Down
76 changes: 76 additions & 0 deletions frontend/app/src/components/chat-engine/edit-is-default-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use client';

import { type ChatEngine, updateChatEngine } from '@/api/chat-engines';
import { useManagedDialog } from '@/components/managed-dialog';
import { Button } from '@/components/ui/button';
import { DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Form, FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/components/ui/form';
import { Switch } from '@/components/ui/switch';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';

const schema = z.object({
is_default: z.literal(true),
});

export interface EditIsDefaultFormProps {
chatEngine: ChatEngine;
}

export function EditIsDefaultForm ({ chatEngine }: EditIsDefaultFormProps) {
const router = useRouter();
const [transitioning, startTransition] = useTransition();
const { setOpen } = useManagedDialog();

const form = useForm<{ is_default: boolean }>({
resolver: zodResolver(schema),
disabled: chatEngine.is_default,
defaultValues: {
is_default: chatEngine.is_default,
},
});

const handleSubmit = form.handleSubmit(async (data) => {
await updateChatEngine(chatEngine.id, data);
startTransition(() => {
router.refresh();
});
toast('ChatEngine\'s is_default successfully updated.');
setOpen(false);
});

return (
<>
<DialogHeader>
<DialogTitle>Update chat engine&#39;s is_default</DialogTitle>
</DialogHeader>
<Form {...form}>
<form id="update-form" className="space-y-4" onSubmit={handleSubmit}>
<FormField
name="is_default"
render={({ field }) => (
<FormItem>
<FormControl>
<Switch name={field.name} onBlur={field.onBlur} checked={field.value} onCheckedChange={field.onChange} disabled={field.disabled}></Switch>
</FormControl>
{chatEngine.is_default && <FormDescription>
Cannot unset default chat engine. Please set another chat engine to default.
</FormDescription>}
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<DialogFooter>
<Button type="submit" form="update-form" disabled={form.formState.disabled || form.formState.isSubmitting || transitioning}>
Update
</Button>
</DialogFooter>
</>
);
}
98 changes: 98 additions & 0 deletions frontend/app/src/components/chat-engine/edit-llm-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client';

import { type ChatEngine, updateChatEngine } from '@/api/chat-engines';
import { listLlms } from '@/api/llms';
import { LlmInfo } from '@/components/llm/LlmInfo';
import { useManagedDialog } from '@/components/managed-dialog';
import { Button } from '@/components/ui/button';
import { DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import useSWR from 'swr';
import { z } from 'zod';

const schema = z.object({
llm: z.number(),
});

export interface EditLlmFormProps {
chatEngine: ChatEngine;
type: 'llm' | 'fast_llm';
}

export function EditLlmForm ({ type, chatEngine }: EditLlmFormProps) {
const router = useRouter();
const [transitioning, startTransition] = useTransition();
const { setOpen } = useManagedDialog();
const { data: llms } = useSWR('api.llms.list-all', () => listLlms({ size: 100 }));

const form = useForm<{ llm: number }>({
resolver: zodResolver(schema),
defaultValues: {
llm: (type === 'llm' ? chatEngine.llm_id : chatEngine.fast_llm_id) ?? undefined,
},
});

const handleSubmit = form.handleSubmit(async (data) => {
await updateChatEngine(chatEngine.id, {
[type === 'llm' ? 'llm_id' : 'fast_llm_id']: data.llm,
});
startTransition(() => {
router.refresh();
});
toast(`ChatEngine's ${type} successfully updated.`);
setOpen(false);
});

return (
<>
<DialogHeader>
<DialogTitle>Update chat engine&#39;s {type}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form id="update-form" className="space-y-4" onSubmit={handleSubmit}>
<FormField
name="llm"
render={({ field }) => (
<FormItem>
<FormLabel>{type === 'llm' ? 'LLM' : 'Fast LLM'}</FormLabel>
<FormControl>
<Select
name={field.name}
disabled={!llms || field.disabled}
value={String(field.value)}
onValueChange={value => field.onChange(parseInt(value))}
>
<SelectTrigger>
<span>
<LlmInfo reverse id={field.value} />
</span>
</SelectTrigger>
<SelectContent>
{llms?.items.map(llm => (
<SelectItem value={String(llm.id)} key={llm.id}>
{llm.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<DialogFooter>
<Button type="submit" form="update-form" disabled={form.formState.disabled || form.formState.isSubmitting || transitioning}>
Update
</Button>
</DialogFooter>
</>
);
}
73 changes: 73 additions & 0 deletions frontend/app/src/components/chat-engine/edit-name-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use client';

import { type ChatEngine, updateChatEngine } from '@/api/chat-engines';
import { useManagedDialog } from '@/components/managed-dialog';
import { Button } from '@/components/ui/button';
import { DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';

const schema = z.object({
name: z.string().min(1),
});

export interface EditNameFormProps {
chatEngine: ChatEngine;
}

export function EditNameForm ({ chatEngine }: EditNameFormProps) {
const router = useRouter();
const [transitioning, startTransition] = useTransition();
const { setOpen } = useManagedDialog();

const form = useForm<{ name: string }>({
resolver: zodResolver(schema),
defaultValues: {
name: chatEngine.name,
},
});

const handleSubmit = form.handleSubmit(async (data) => {
await updateChatEngine(chatEngine.id, data);
startTransition(() => {
router.refresh();
});
toast('ChatEngine\'s name successfully updated.');
setOpen(false);
});

return (
<>
<DialogHeader>
<DialogTitle>Update chat engine&#39;s name</DialogTitle>
</DialogHeader>
<Form {...form}>
<form id="update-form" className="space-y-4" onSubmit={handleSubmit}>
<FormField
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<DialogFooter>
<Button type="submit" form='update-form' disabled={form.formState.disabled || form.formState.isSubmitting || transitioning}>
Update
</Button>
</DialogFooter>
</>
);
}
26 changes: 26 additions & 0 deletions frontend/app/src/components/llm/LlmInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client';

import { useLlm } from '@/components/llm/hooks';
import { Badge, badgeVariants } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { Loader2Icon } from 'lucide-react';
import Link from 'next/link';

export function LlmInfo ({ reverse = false, id }: { reverse?: boolean, id: number | undefined | null }) {
const { llm, isLoading } = useLlm(id);

if (isLoading) {
return <Loader2Icon className='size-4 animate-spin repeat-infinite'/>
}

if (!llm) {
return <span>N/A</span>;
}

return (
<span className={cn('flex gap-1 items-center', reverse && 'flex-row-reverse')}>
<Badge variant="secondary"><span className='font-bold'>{llm.provider}</span>:<span className='opacity-50'>{llm.model}</span></Badge>
<Link className={badgeVariants()} href={`/llms/${llm.id}`}>{llm.name}</Link>
</span>
);
}
7 changes: 7 additions & 0 deletions frontend/app/src/components/llm/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getLlm } from '@/api/llms';
import useSWR from 'swr';

export function useLlm (id: number | null | undefined) {
const { data: llm, ...rest } = useSWR(id == null ? null : `api.llm.get?id=${id}`, () => getLlm(id as number));
return { llm, ...rest };
}
Loading

0 comments on commit 80c7e19

Please sign in to comment.