diff --git a/frontend/app/src/api/datasources.ts b/frontend/app/src/api/datasources.ts index b039d6631..d2ac5750b 100644 --- a/frontend/app/src/api/datasources.ts +++ b/frontend/app/src/api/datasources.ts @@ -8,16 +8,6 @@ export interface DatasourceBase { name: string; } -interface DeprecatedDatasourceBase extends DatasourceBase { - created_at: Date; - updated_at: Date; - user_id: string | null; - build_kg_index: boolean; - llm_id: number | null; -} - -export type DeprecatedDatasource = DeprecatedDatasourceBase & DatasourceSpec - type DatasourceSpec = ({ data_source_type: 'file' config: { file_id: number, file_name: string }[] @@ -43,18 +33,6 @@ export interface BaseCreateDatasourceParams { name: string; } -export interface DeprecatedBaseCreateDatasourceParams extends BaseCreateDatasourceParams { - description: string; - /** - * @deprecated - */ - build_kg_index: boolean; - /** - * @deprecated - */ - llm_id: number | null; -} - export type CreateDatasourceSpecParams = ({ data_source_type: 'file' config: { file_id: number, file_name: string }[] @@ -66,7 +44,7 @@ export type CreateDatasourceSpecParams = ({ config: { url: string } }); -export type CreateDatasourceParams = DeprecatedBaseCreateDatasourceParams & CreateDatasourceSpecParams; +export type CreateDatasourceParams = BaseCreateDatasourceParams & CreateDatasourceSpecParams; export interface Upload { created_at?: Date; @@ -92,16 +70,6 @@ export type DatasourceKgIndexError = { error: string | null } -const deprecatedBaseDatasourceSchema = z.object({ - id: z.number(), - name: z.string(), - created_at: zodJsonDate(), - updated_at: zodJsonDate(), - user_id: z.string().nullable(), - build_kg_index: z.boolean(), - llm_id: z.number().nullable(), -}); - const datasourceSpecSchema = z.discriminatedUnion('data_source_type', [ z.object({ data_source_type: z.literal('file'), @@ -123,9 +91,6 @@ const datasourceSpecSchema = z.discriminatedUnion('data_source_type', [ })], ) satisfies ZodType; -export const deprecatedDatasourceSchema = deprecatedBaseDatasourceSchema - .and(datasourceSpecSchema) satisfies ZodType; - export const datasourceSchema = z.object({ id: z.number(), name: z.string(), @@ -141,8 +106,7 @@ const uploadSchema = z.object({ created_at: zodJsonDate().optional(), updated_at: zodJsonDate().optional(), }) satisfies ZodType; - -const datasourceOverviewSchema = z.object({ +z.object({ vector_index: indexSchema, documents: totalSchema, chunks: totalSchema, @@ -150,34 +114,45 @@ const datasourceOverviewSchema = z.object({ relationships: totalSchema.optional(), }) satisfies ZodType; -export async function listDataSources ({ page = 1, size = 10 }: PageParams = {}): Promise> { - return fetch(requestUrl('/api/v1/admin/datasources', { page, size }), { +export async function listDataSources (kbId: number, { page = 1, size = 10 }: PageParams = {}): Promise> { + return fetch(requestUrl(`/api/v1/admin/knowledge_bases/${kbId}/datasources`, { page, size }), { headers: await authenticationHeaders(), - }).then(handleResponse(zodPage(deprecatedDatasourceSchema))); + }).then(handleResponse(zodPage(datasourceSchema))); } -export async function getDatasource (id: number): Promise { - return fetch(requestUrl(`/api/v1/admin/datasources/${id}`), { +export async function getDatasource (kbId: number, id: number): Promise { + return fetch(requestUrl(`/api/v1/admin/knowledge_bases/${kbId}/datasources/${id}`), { headers: await authenticationHeaders(), - }).then(handleResponse(deprecatedDatasourceSchema)); + }).then(handleResponse(datasourceSchema)); } -export async function deleteDatasource (id: number): Promise { - await fetch(requestUrl(`/api/v1/admin/datasources/${id}`), { +export async function deleteDatasource (kbId: number, id: number): Promise { + await fetch(requestUrl(`/api/v1/admin/knowledge_bases/${kbId}/datasources/${id}`), { method: 'DELETE', headers: await authenticationHeaders(), }).then(handleErrors); } -export async function createDatasource (params: CreateDatasourceParams) { - return fetch(requestUrl(`/api/v1/admin/datasources`), { +export async function createDatasource (kbId: number, params: CreateDatasourceParams) { + return fetch(requestUrl(`/api/v1/admin/knowledge_bases/${kbId}/datasources`), { method: 'POST', headers: { ...await authenticationHeaders(), 'Content-Type': 'application/json', }, body: JSON.stringify(params), - }).then(handleResponse(deprecatedDatasourceSchema)); + }).then(handleResponse(datasourceSchema)); +} + +export async function updateDatasource (kbId: number, id: number, params: { name: string }) { + return fetch(requestUrl(`/api/v1/admin/knowledge_bases/${kbId}/datasources/${id}`), { + method: 'PUT', + headers: { + ...await authenticationHeaders(), + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }).then(handleResponse(datasourceSchema)); } export async function uploadFiles (files: File[]) { diff --git a/frontend/app/src/app/(main)/(admin)/datasources/[id]/documents/page.tsx b/frontend/app/src/app/(main)/(admin)/datasources/[id]/documents/page.tsx deleted file mode 100644 index 685e69e7b..000000000 --- a/frontend/app/src/app/(main)/(admin)/datasources/[id]/documents/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { getDatasource } from '@/api/datasources'; -import { AdminPageHeading } from '@/components/admin-page-heading'; -import { DatasourceDeprecationAlert } from '@/components/datasource/DatasourceDeprecationAlert'; -import { DocumentsTable } from '@/components/documents/documents-table'; -import { isServerError } from '@/lib/request'; -import { notFound } from 'next/navigation'; -import { cache } from 'react'; - -export default async function ChatEnginesPage ({ params }: { params: { id: string } }) { - const datasource = await cachedGetDatasource(parseInt(params.id)); - - return ( - <> - - - - - ); -} - -const cachedGetDatasource = cache(async (id: number) => { - try { - return await getDatasource(id); - } catch (error) { - if (isServerError(error, [404])) { - notFound(); - } else { - return Promise.reject(error); - } - } -}); - diff --git a/frontend/app/src/app/(main)/(admin)/datasources/[id]/page.tsx b/frontend/app/src/app/(main)/(admin)/datasources/[id]/page.tsx deleted file mode 100644 index 1714c4aaa..000000000 --- a/frontend/app/src/app/(main)/(admin)/datasources/[id]/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { AdminPageHeading } from '@/components/admin-page-heading'; -import { DatasourceDeprecationAlert } from '@/components/datasource/DatasourceDeprecationAlert'; -import { DatasourceDetails } from '@/components/datasource/DatasourceDetails'; -import { DatasourceName } from '@/components/datasource/DatasourceName'; - -export default function DatasourcePage ({ params }: { params: { id: string } }) { - const id = parseInt(params.id); - - return ( -
- , url: `/datasources/${id}` }, - ]} - /> - - -
- ); -} \ No newline at end of file diff --git a/frontend/app/src/app/(main)/(admin)/datasources/page.tsx b/frontend/app/src/app/(main)/(admin)/datasources/page.tsx deleted file mode 100644 index 2b5083629..000000000 --- a/frontend/app/src/app/(main)/(admin)/datasources/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { AdminPageHeading } from '@/components/admin-page-heading'; -import { DatasourceDeprecationAlert } from '@/components/datasource/DatasourceDeprecationAlert'; -import { DatasourceTable } from '@/components/datasource/DatasourceTable'; - -export default function ChatEnginesPage () { - return ( - <> - - - - - ); -} diff --git a/frontend/app/src/app/(main)/(admin)/documents/page.tsx b/frontend/app/src/app/(main)/(admin)/documents/page.tsx deleted file mode 100644 index e15427939..000000000 --- a/frontend/app/src/app/(main)/(admin)/documents/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { AdminPageHeading } from '@/components/admin-page-heading'; -import { DocumentDeprecationAlert } from '@/components/documents/DocumentDeprecationAlert'; -import { DocumentsTable } from '@/components/documents/documents-table'; - -export default function DocumentsPage () { - return ( - <> - - - - - ); -} - diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(special)/data-sources/new/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(special)/data-sources/new/page.tsx new file mode 100644 index 000000000..cec0558d9 --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(special)/data-sources/new/page.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { AdminPageHeading } from '@/components/admin-page-heading'; +import { CreateDatasourceForm } from '@/components/datasource/create-datasource-form'; +import { mutateKnowledgeBases, useKnowledgeBase } from '@/components/knowledge-base/hooks'; +import { Loader2Icon } from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +export default function NewKnowledgeBaseDataSourcePage ({ params }: { params: { id: string } }) { + const id = parseInt(decodeURIComponent(params.id)); + const { knowledgeBase } = useKnowledgeBase(id); + const router = useRouter(); + + return ( + <> + , url: `/knowledge-bases/${id}` }, + { title: 'DataSources', url: `/knowledge-bases/${id}/data-sources` }, + { title: 'New' }, + ]} + /> + { + router.back(); + mutateKnowledgeBases(); + }} + /> + + ); +} diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/data-sources/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/data-sources/page.tsx new file mode 100644 index 000000000..20d3efa1b --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/data-sources/page.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { DatasourceCard } from '@/components/datasource/datasource-card'; +import { DatasourceCreateOption } from '@/components/datasource/datasource-create-option'; +import { NoDatasourcePlaceholder } from '@/components/datasource/no-datasource-placeholder'; +import { useAllKnowledgeBaseDataSources } from '@/components/knowledge-base/hooks'; +import { Skeleton } from '@/components/ui/skeleton'; +import { FileDownIcon, GlobeIcon, PaperclipIcon } from 'lucide-react'; + +export default function KnowledgeBaseDataSourcesPage ({ params }: { params: { id: string } }) { + const id = parseInt(decodeURIComponent(params.id)); + const { data: dataSources, isLoading } = useAllKnowledgeBaseDataSources(id); + + return ( +
+
+

Create Data Source

+
+ } + title="Files" + > + Upload files + + } + title="Web Pages" + > + Select pages. + + } + title="Website by sitemap" + > + Select web sitemap. + +
+
+
+

Browse existing Data Sources

+ {isLoading && } + {dataSources?.map(datasource => ( + + ))} + {dataSources?.length === 0 && ( + + )} +
+
+ ); +} diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/index-progress/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/index-progress/page.tsx new file mode 100644 index 000000000..af956e9b8 --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/index-progress/page.tsx @@ -0,0 +1,12 @@ +import { KnowledgeBaseIndexProgress } from '@/components/knowledge-base/knowledge-base-index'; + +export default function KnowledgeBaseIndexProgressPage ({ params }: { params: { id: string } }) { + const id = parseInt(decodeURIComponent(params.id)); + + return ( +
+

Index Progress

+ +
+ ); +} \ No newline at end of file diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/knowledge-graph-explorer/create-synopsis-entity/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/knowledge-graph-explorer/create-synopsis-entity/page.tsx similarity index 96% rename from frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/knowledge-graph-explorer/create-synopsis-entity/page.tsx rename to frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/knowledge-graph-explorer/create-synopsis-entity/page.tsx index fb6149b4d..9923bc5c4 100644 --- a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/knowledge-graph-explorer/create-synopsis-entity/page.tsx +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/knowledge-graph-explorer/create-synopsis-entity/page.tsx @@ -15,7 +15,7 @@ export default function CreateSynopsisEntityPage ({ params }: { params: { id: st return ( <> - + Back diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/knowledge-graph-explorer/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/knowledge-graph-explorer/page.tsx new file mode 100644 index 000000000..ab8f4e7f7 --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/knowledge-graph-explorer/page.tsx @@ -0,0 +1,11 @@ +import { GraphEditor } from '@/components/graph/GraphEditor'; + +export default function KnowledgeGraphExplorerPage ({ params }: { params: { id: string } }) { + const id = parseInt(decodeURIComponent(params.id)); + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/layout.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/layout.tsx new file mode 100644 index 000000000..a7c529d6a --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/layout.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { KnowledgeBaseTabs } from '@/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/tabs'; +import { AdminPageHeading } from '@/components/admin-page-heading'; +import { ArrowRightIcon } from '@/components/icons'; +import { useKnowledgeBase } from '@/components/knowledge-base/hooks'; +import { SecondaryNavigatorLayout, SecondaryNavigatorList, SecondaryNavigatorMain } from '@/components/secondary-navigator-list'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { AlertTriangleIcon, Loader2Icon } from 'lucide-react'; +import Link from 'next/link'; +import type { ReactNode } from 'react'; + +export default function KnowledgeBaseLayout ({ params, children }: { params: { id: string }, children: ReactNode }) { + const id = parseInt(decodeURIComponent(params.id)); + const { knowledgeBase } = useKnowledgeBase(id); + + return ( + <> + + {knowledgeBase?.data_sources_total === 0 && ( + + + + + + +

This Knowledge Base has no datasource.

+ + Create Data Source + + +
+
+
+ )} + + {knowledgeBase?.name ?? } + + + ), + }, + ]} + /> + + + + + + {children} + + + + ); +} \ No newline at end of file diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/page.tsx similarity index 100% rename from frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/page.tsx rename to frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/page.tsx diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/settings/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/settings/page.tsx similarity index 100% rename from frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/settings/page.tsx rename to frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/settings/page.tsx diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/tabs.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/tabs.tsx new file mode 100644 index 000000000..75ab7952e --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/(tabs)/tabs.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useKnowledgeBase } from '@/components/knowledge-base/hooks'; +import { SecondaryNavigatorLink } from '@/components/secondary-navigator-list'; + +export function KnowledgeBaseTabs ({ knowledgeBaseId }: { knowledgeBaseId: number }) { + const { knowledgeBase } = useKnowledgeBase(knowledgeBaseId); + + return ( + <> + + Documents + + {knowledgeBase?.documents_total} + + + + Data Sources + + {knowledgeBase?.data_sources_total} + + + + Index Progress + + {/* startTransition(() => {*/} + {/* router.push(`/knowledge-bases/${knowledgeBase.id}/retrieval-tester`);*/} + {/* })}*/} + {/*>*/} + {/* Retrieval Tester*/} + {/**/} + + Graph Explorer + + + Settings + + + ); +} \ No newline at end of file diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/context.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/context.tsx index f8a331e6f..6658940c9 100644 --- a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/context.tsx +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/context.tsx @@ -12,7 +12,3 @@ export function KBProvider ({ children, value }: { children: ReactNode, value: K ); } - -export function useKB () { - return useContext(KBContext); -} diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/data-sources/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/data-sources/page.tsx deleted file mode 100644 index b2c81b505..000000000 --- a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/data-sources/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { cachedGetKnowledgeBaseById } from '@/app/(main)/(admin)/knowledge-bases/[id]/api'; -import { KBProvider } from '@/app/(main)/(admin)/knowledge-bases/[id]/context'; -import { KnowledgeBaseDatasourceDetails } from '@/components/knowledge-base/datasource-details'; - -export default async function KnowledgeBaseDataSourcesPage ({ params }: { params: { id: string } }) { - const id = parseInt(decodeURIComponent(params.id)); - const kb = await cachedGetKnowledgeBaseById(id); - - return ( - -
-

Data Sources

-
- {kb.data_sources.map(datasource => ( -
-

- {datasource.name} -

-
- -
-
- ))} -
-
-
- ); -} \ No newline at end of file diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/index-progress/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/index-progress/page.tsx deleted file mode 100644 index c2de24db9..000000000 --- a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/index-progress/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { cachedGetKnowledgeBaseById } from '@/app/(main)/(admin)/knowledge-bases/[id]/api'; -import { KBProvider } from '@/app/(main)/(admin)/knowledge-bases/[id]/context'; -import { KnowledgeBaseIndexProgress } from '@/components/knowledge-base/knowledge-base-index'; - -export default async function KnowledgeBaseIndexProgressPage ({ params }: { params: { id: string } }) { - const id = parseInt(decodeURIComponent(params.id)); - const kb = await cachedGetKnowledgeBaseById(id); - - return ( - -
-

Index Progress

- -
-
- ); -} \ No newline at end of file diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/knowledge-graph-explorer/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/knowledge-graph-explorer/page.tsx deleted file mode 100644 index 4c3b124a8..000000000 --- a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/knowledge-graph-explorer/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { cachedGetKnowledgeBaseById } from '@/app/(main)/(admin)/knowledge-bases/[id]/api'; -import { KBProvider } from '@/app/(main)/(admin)/knowledge-bases/[id]/context'; -import { GraphEditor } from '@/components/graph/GraphEditor'; - -export default async function KnowledgeGraphExplorerPage ({ params }: { params: { id: string } }) { - const id = parseInt(decodeURIComponent(params.id)); - const kb = await cachedGetKnowledgeBaseById(id); - - return ( - -
- -
-
- ); -} \ No newline at end of file diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/layout.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/layout.tsx deleted file mode 100644 index 827d6c653..000000000 --- a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { cachedGetKnowledgeBaseById } from '@/app/(main)/(admin)/knowledge-bases/[id]/api'; -import { KnowledgeBaseTabs } from '@/app/(main)/(admin)/knowledge-bases/[id]/tabs'; -import { AdminPageHeading } from '@/components/admin-page-heading'; -import type { ReactNode } from 'react'; - -export default async function KnowledgeBaseLayout ({ params, children }: { params: { id: string }, children: ReactNode }) { - const id = parseInt(decodeURIComponent(params.id)); - const kb = await cachedGetKnowledgeBaseById(id); - - return ( - <> - - - {children} - - ); -} \ No newline at end of file diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/tabs.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/tabs.tsx deleted file mode 100644 index c0cb23ee7..000000000 --- a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/tabs.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client'; - -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { useRouter, useSelectedLayoutSegments } from 'next/navigation'; -import { useTransition } from 'react'; - -export function KnowledgeBaseTabs ({ id }: { id: number }) { - const router = useRouter(); - const [transitioning, startTransition] = useTransition(); - const segments = useSelectedLayoutSegments(); - - const segment = segments?.[0] ?? ''; - - return ( - - - startTransition(() => { - router.push(`/knowledge-bases/${id}`); - })} - > - Documents - - startTransition(() => { - router.push(`/knowledge-bases/${id}/data-sources`); - })} - > - Data Sources - - startTransition(() => { - router.push(`/knowledge-bases/${id}/index-progress`); - })} - > - Index Progress - - startTransition(() => { - router.push(`/knowledge-bases/${id}/retrieval-tester`); - })} - > - Retrieval Tester - - startTransition(() => { - router.push(`/knowledge-bases/${id}/knowledge-graph-explorer`); - })} - > - Knowledge Graph Explorer - - startTransition(() => { - router.push(`/knowledge-bases/${id}/settings`); - })} - > - Settings - - - - ); -} \ No newline at end of file diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/page.tsx index 47476ca11..b27f22757 100644 --- a/frontend/app/src/app/(main)/(admin)/knowledge-bases/page.tsx +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/page.tsx @@ -2,15 +2,12 @@ import { AdminPageHeading } from '@/components/admin-page-heading'; import KnowledgeBaseEmptyState from '@/components/knowledge-base/empty-state'; -import { useKnowledgeBases } from '@/components/knowledge-base/hooks'; +import { useAllKnowledgeBases } from '@/components/knowledge-base/hooks'; import { KnowledgeBaseCard, KnowledgeBaseCardPlaceholder } from '@/components/knowledge-base/knowledge-base-card'; import { NextLink } from '@/components/nextjs/NextLink'; -import type { PaginationState } from '@tanstack/table-core'; -import { useState } from 'react'; export default function KnowledgeBasesPage () { - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); - const { knowledgeBases, isLoading } = useKnowledgeBases(pagination.pageIndex, pagination.pageSize); + const { data: knowledgeBases, isLoading } = useAllKnowledgeBases(); return ( <> @@ -25,9 +22,9 @@ export default function KnowledgeBasesPage () { { isLoading ?
- : !!knowledgeBases?.items.length + : !!knowledgeBases?.length ?
- {knowledgeBases?.items.map(kb => ( + {knowledgeBases.map(kb => ( ))}
diff --git a/frontend/app/src/app/(main)/(admin)/site-settings/layout.tsx b/frontend/app/src/app/(main)/(admin)/site-settings/layout.tsx index 7876fa0f6..512ebebc0 100644 --- a/frontend/app/src/app/(main)/(admin)/site-settings/layout.tsx +++ b/frontend/app/src/app/(main)/(admin)/site-settings/layout.tsx @@ -24,7 +24,7 @@ export default function SiteSettingsLayout ({ children }: { children: ReactNode JS Widget - + {children} diff --git a/frontend/app/src/app/(main)/nav.tsx b/frontend/app/src/app/(main)/nav.tsx index f98e44e85..c9974ad3d 100644 --- a/frontend/app/src/app/(main)/nav.tsx +++ b/frontend/app/src/app/(main)/nav.tsx @@ -1,13 +1,13 @@ 'use client'; import { logout } from '@/api/auth'; -import { listChatEngines } from '@/api/chat-engines'; import type { PublicWebsiteSettings } from '@/api/site-settings'; import { useAuth } from '@/components/auth/AuthProvider'; import { Branding } from '@/components/branding'; +import { useAllChatEngines } from '@/components/chat-engine/hooks'; import { ChatNewDialog } from '@/components/chat/chat-new-dialog'; import { ChatsHistory } from '@/components/chat/chats-history'; -import { useKnowledgeBases } from '@/components/knowledge-base/hooks'; +import { useAllKnowledgeBases } from '@/components/knowledge-base/hooks'; import { type NavGroup, SiteNav } from '@/components/site-nav'; import { useBootstrapStatus } from '@/components/system/BootstrapStatusProvider'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; @@ -17,12 +17,11 @@ import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from '@/compone import { Skeleton } from '@/components/ui/skeleton'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { useHref } from '@/components/use-href'; -import { ActivitySquareIcon, AlertTriangleIcon, BinaryIcon, BotMessageSquareIcon, BrainCircuitIcon, CogIcon, ComponentIcon, FilesIcon, HomeIcon, KeyRoundIcon, LibraryBigIcon, LibraryIcon, LogInIcon, MessageCircleQuestionIcon, MessagesSquareIcon, ShuffleIcon } from 'lucide-react'; +import { ActivitySquareIcon, AlertTriangleIcon, BinaryIcon, BotMessageSquareIcon, BrainCircuitIcon, CogIcon, ComponentIcon, HomeIcon, KeyRoundIcon, LibraryBigIcon, LogInIcon, MessageCircleQuestionIcon, MessagesSquareIcon, ShuffleIcon } from 'lucide-react'; import NextLink from 'next/link'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import type { ReactNode } from 'react'; -import useSWR from 'swr'; export function SiteSidebar ({ setting }: { setting: PublicWebsiteSettings }) { return ( @@ -84,15 +83,6 @@ function NavContent () { ], sectionProps: { className: 'mt-auto mb-0' }, }); - - groups.push({ - title: 'Legacy', - items: [ - { href: '/documents', title: 'Documents', icon: FilesIcon }, - { href: '/datasources', title: 'Datasources', icon: LibraryIcon, details: !required.datasource && You need to configure at least one Datasource. }, - ], - sectionProps: { className: 'mt-auto mb-0' }, - }); } if (user?.is_superuser) { @@ -176,21 +166,21 @@ function CountSpan ({ children }: { children?: ReactNode }) { } function KnowledgeBaseNavDetails () { - const { knowledgeBases, isLoading } = useKnowledgeBases(0, 10); + const { data: knowledgeBases, isLoading } = useAllKnowledgeBases(); if (isLoading) { return ; } - return {knowledgeBases?.total}; + return {knowledgeBases?.length}; } function ChatEnginesNavDetails () { - const { data, isLoading } = useSWR('api.chat-engines.list-all', () => listChatEngines({ page: 1, size: 100 })); + const { data, isLoading } = useAllChatEngines(); if (isLoading) { return ; } - return {data?.total}; + return {data?.length}; } diff --git a/frontend/app/src/components/cells/reference.tsx b/frontend/app/src/components/cells/reference.tsx index d3c2494c6..2fca0e8da 100644 --- a/frontend/app/src/components/cells/reference.tsx +++ b/frontend/app/src/components/cells/reference.tsx @@ -3,7 +3,7 @@ import Link from 'next/link'; export function DatasourceCell ({ id, name }: { id: number, name: string }) { - return {name}; + return {name}; } export function KnowledgeBaseCell ({ id, name }: { id?: number, name?: string }) { diff --git a/frontend/app/src/components/chat-engine/hooks.ts b/frontend/app/src/components/chat-engine/hooks.ts new file mode 100644 index 000000000..574087529 --- /dev/null +++ b/frontend/app/src/components/chat-engine/hooks.ts @@ -0,0 +1,7 @@ +import { listChatEngines } from '@/api/chat-engines'; +import { listAllHelper } from '@/lib/request'; +import useSWR from 'swr'; + +export function useAllChatEngines () { + return useSWR('api.chat-engines.list-all', () => listAllHelper(listChatEngines, 'id')); +} \ No newline at end of file diff --git a/frontend/app/src/components/chat/chat-controller.ts b/frontend/app/src/components/chat/chat-controller.ts index 5f9947b23..710d0672e 100644 --- a/frontend/app/src/components/chat/chat-controller.ts +++ b/frontend/app/src/components/chat/chat-controller.ts @@ -300,7 +300,7 @@ export class ChatController listChatEngines({ page: 1, size: 100 })); + const { data, isLoading } = useAllChatEngines(); return (
@@ -66,11 +67,11 @@ export function MessageInput ({ - {data?.items.map(item => ( + {data?.map(item => ( {item.is_default ? default : item.name} - {item.engine_options.external_engine_config + {!!item.engine_options.external_engine_config?.stream_chat_api_url ? External Engine (StackVM) : item.engine_options.knowledge_graph?.enabled !== false /* TODO: require default config */ ? Knowledge graph enabled diff --git a/frontend/app/src/components/datasource/DatasourceDeprecationAlert.tsx b/frontend/app/src/components/datasource/DatasourceDeprecationAlert.tsx deleted file mode 100644 index 0e287f330..000000000 --- a/frontend/app/src/components/datasource/DatasourceDeprecationAlert.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { AlertCircleIcon } from 'lucide-react'; - -export function DatasourceDeprecationAlert () { - return ( - - - - Bare Datasource management was deprecated. - - - TiDB.ai now uses Knowledge Base to manage multiple datasources. This page will be removed soon. - - - ); -} \ No newline at end of file diff --git a/frontend/app/src/components/datasource/DatasourceDetails.tsx b/frontend/app/src/components/datasource/DatasourceDetails.tsx deleted file mode 100644 index d8c2b6190..000000000 --- a/frontend/app/src/components/datasource/DatasourceDetails.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import { useDatasource } from '@/components/datasource/hooks'; -import { DateFormat } from '@/components/date-format'; -import { OptionDetail } from '@/components/option-detail'; -import { Badge } from '@/components/ui/badge'; - -export function DatasourceDetails ({ id }: { id: number }) { - return ( - <> - - - - ); -} - -function DatasourceFields ({ id }: { id: number }) { - const { datasource } = useDatasource(id); - return ( -
- - - } /> - } /> - {(datasource?.data_source_type === 'web_sitemap') && ( - - )} - {(datasource?.data_source_type === 'web_single_page') && ( - {datasource?.config.urls.map(url =>
  • {url}
  • )}} /> - )} - -
    - ); -} - -function DatasourceUploadFiles ({ id }: { id: number }) { - const { datasource } = useDatasource(id); - - if (datasource?.data_source_type !== 'file' || datasource.config.length === 0) { - return null; - } - return ( -
    -

    Files

    -
    - {datasource.config.map(file => ( - - - {file.file_name} - - #{file.file_id} - - ))} -
    -
    - ); -} diff --git a/frontend/app/src/components/datasource/DatasourceName.tsx b/frontend/app/src/components/datasource/DatasourceName.tsx deleted file mode 100644 index 6e6684c14..000000000 --- a/frontend/app/src/components/datasource/DatasourceName.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client'; - -import { getDatasource } from '@/api/datasources'; -import { cn } from '@/lib/utils'; -import { Loader2Icon } from 'lucide-react'; -import useSWR from 'swr'; - -export function DatasourceName ({ id }: { id: number }) { - const { data: datasource, isLoading, isValidating } = useSWR(`api.datasources.${id}`, () => getDatasource(id)); - - if (isLoading) { - return ; - } - return {datasource?.name ?? '(Unknown Datasource)'}; -} diff --git a/frontend/app/src/components/datasource/DatasourceTable.tsx b/frontend/app/src/components/datasource/DatasourceTable.tsx deleted file mode 100644 index f84939f1f..000000000 --- a/frontend/app/src/components/datasource/DatasourceTable.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client'; - -import { deleteDatasource, type DeprecatedDatasource, listDataSources } from '@/api/datasources'; -import { actions } from '@/components/cells/actions'; -import { link } from '@/components/cells/link'; -import { DataTableRemote } from '@/components/data-table-remote'; -import { LlmInfo } from '@/components/llm/LlmInfo'; -import type { ColumnDef } from '@tanstack/react-table'; -import { createColumnHelper } from '@tanstack/table-core'; -import { Trash2Icon } from 'lucide-react'; - -const helper = createColumnHelper(); - -const columns = [ - helper.accessor('name', { cell: link({ url: datasource => `/datasources/${datasource.id}` }) }), - helper.accessor('data_source_type', {}), - helper.accessor('llm_id', { cell: (ctx) => }), - helper.accessor('build_kg_index', {}), - helper.accessor('user_id', {}), - helper.display({ - header: 'Actions', - cell: actions(datasource => [ - { - key: 'delete', - title: 'Delete', - icon: , - dangerous: {}, - action: async ({ table }) => { - await deleteDatasource(datasource.id); - table.reload?.(); - }, - }, - ]), - }), -] as ColumnDef[]; - -export function DatasourceTable () { - return ( - - ); -} diff --git a/frontend/app/src/components/datasource/create-datasource-form.tsx b/frontend/app/src/components/datasource/create-datasource-form.tsx new file mode 100644 index 000000000..2caabb954 --- /dev/null +++ b/frontend/app/src/components/datasource/create-datasource-form.tsx @@ -0,0 +1,187 @@ +import { type BaseCreateDatasourceParams, createDatasource, type CreateDatasourceSpecParams, uploadFiles } from '@/api/datasources'; +import { FormInput } from '@/components/form/control-widget'; +import { FormFieldBasicLayout, FormPrimitiveArrayFieldBasicLayout } from '@/components/form/field-layout'; +import { handleSubmitHelper } from '@/components/form/utils'; +import { FilesInput } from '@/components/form/widgets/FilesInput'; +import { Button } from '@/components/ui/button'; +import { Form, FormField, FormItem, FormLabel } from '@/components/ui/form'; +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; +import { zodFile } from '@/lib/zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FileDownIcon, GlobeIcon, PaperclipIcon } from 'lucide-react'; +import { useSearchParams } from 'next/navigation'; +import { useForm, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; + +const types = ['file', 'web_single_page', 'web_sitemap'] as const; + +const isType = (value: string | null): value is typeof types[number] => types.includes(value as any); + +export function CreateDatasourceForm ({ knowledgeBaseId, onCreated }: { knowledgeBaseId: number, onCreated?: () => void }) { + const usp = useSearchParams()!; + const uType = usp.get('type'); + + const form = useForm({ + resolver: zodResolver(createDatasourceSchema), + defaultValues: switchDatasource({ + data_source_type: 'file', + name: '', + files: [], + }, isType(uType) ? uType : 'file'), + }); + + const handleSubmit = handleSubmitHelper(form, async (ds) => { + const createParams = await preCreate(ds); + await createDatasource(knowledgeBaseId, createParams); + onCreated?.(); + }); + + return ( +
    + + ( + + + Data Source Type + + { + form.reset(switchDatasource(form.getValues(), value as never)); + })} + onBlur={field.onBlur} + > + + + File + + + + Web Single Page + + + + Web Sitemap + + + + )} + /> + + + + + + + + ); +} + +function SpecFields () { + const { watch: useWatch } = useFormContext(); + const type = useWatch('data_source_type'); + + return ( + <> + {type === 'file' && ( + + + + )} + {type === 'web_single_page' && ( + ''}> + + + )} + {type === 'web_sitemap' && ( + + + + )} + + ); +} + +type CreateDatasourceFormParams = z.infer; + +export const createDatasourceSchema = z.object({ + name: z.string().trim().min(1, 'Must not blank'), +}).and(z.discriminatedUnion('data_source_type', [ + z.object({ + data_source_type: z.literal('file'), + files: zodFile().array().min(1), + }), + z.object({ + data_source_type: z.literal('web_single_page'), + urls: z.string().url().array().min(1), + }), + z.object({ + data_source_type: z.literal('web_sitemap'), + url: z.string().url(), + }), +])); + +function switchDatasource (data: CreateDatasourceFormParams, type: CreateDatasourceSpecParams['data_source_type']): CreateDatasourceFormParams { + if (data.data_source_type === type) { + return data; + } + + switch (type) { + case 'file': + return { + name: data.name, + data_source_type: 'file', + files: [], + }; + case 'web_single_page': + return { + name: data.name, + data_source_type: 'web_single_page', + urls: [], + }; + case 'web_sitemap': + return { + name: data.name, + data_source_type: 'web_sitemap', + url: '', + }; + } +} + +async function preCreate (ds: CreateDatasourceFormParams): Promise { + switch (ds.data_source_type) { + case 'file': { + const { files, ...rest } = ds; + const uploadedFiles = await uploadFiles(ds.files); + return { + ...rest, + config: uploadedFiles.map(f => ({ + file_id: f.id, + file_name: f.name, + })), + }; + } + case 'web_single_page': { + const { urls, ...rest } = ds; + + return { + ...rest, + config: { urls }, + }; + } + + case 'web_sitemap': + const { url, ...rest } = ds; + + return { + ...rest, + config: { url }, + }; + } +} diff --git a/frontend/app/src/components/datasource/datasource-card.tsx b/frontend/app/src/components/datasource/datasource-card.tsx new file mode 100644 index 000000000..fe46d1a37 --- /dev/null +++ b/frontend/app/src/components/datasource/datasource-card.tsx @@ -0,0 +1,108 @@ +'use client'; + +import { type Datasource, deleteDatasource } from '@/api/datasources'; +import { DangerousActionButton } from '@/components/dangerous-action-button'; +import { UpdateDatasourceForm } from '@/components/datasource/update-datasource-form'; +import { ManagedDialog } from '@/components/managed-dialog'; +import { ManagedPanelContext } from '@/components/managed-panel'; +import { Button } from '@/components/ui/button'; +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { FileDownIcon, GlobeIcon, PaperclipIcon } from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +export function DatasourceCard ({ knowledgeBaseId, datasource }: { knowledgeBaseId: number, datasource: Datasource }) { + const router = useRouter(); + + return ( + + + {datasource.name} + + + + + + + + + + + + Configure Datasource + + + + {({ setOpen }) => ( + { + router.refresh(); + setOpen(false); + }} + /> + )} + + + + { + await deleteDatasource(knowledgeBaseId, datasource.id); + }} + asChild + > + + + + + ); +} + +function DatasourceCardDetails ({ datasource }: { datasource: Datasource }) { + return ( + + {(() => { + switch (datasource.data_source_type) { + case 'web_sitemap': + return ; + case 'web_single_page': + return ; + case 'file': + return ; + } + })()} + + {(() => { + switch (datasource.data_source_type) { + case 'web_sitemap': + return datasource.config.url; + case 'web_single_page': + return datasource.config.urls.join(', '); + case 'file': + if (datasource.config.length === 1) { + return datasource.config[0].file_name; + } else { + return ( + <> + {datasource.config[0]?.file_name} + + + +{datasource.config.length - 1} files + + + {datasource.config.slice(1).map(file => ( + {file.file_name} + ))} + + + + ); + } + } + })()} + + + ); +} diff --git a/frontend/app/src/components/datasource/datasource-create-option.tsx b/frontend/app/src/components/datasource/datasource-create-option.tsx new file mode 100644 index 000000000..3da149aec --- /dev/null +++ b/frontend/app/src/components/datasource/datasource-create-option.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { NextLink } from '@/components/nextjs/NextLink'; +import type { ReactNode } from 'react'; + +export function DatasourceCreateOption ({ + knowledgeBaseId, + type, + icon, + title, + children, +}: { + knowledgeBaseId: number + type: string + icon?: ReactNode + title: ReactNode + children?: ReactNode +}) { + return ( + +
    + + {icon} + + {title} +
    +
    + {children} +
    +
    + ); +} diff --git a/frontend/app/src/components/datasource/hooks.ts b/frontend/app/src/components/datasource/hooks.ts deleted file mode 100644 index ebe5b8975..000000000 --- a/frontend/app/src/components/datasource/hooks.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { getDatasource } from '@/api/datasources'; -import useSWR from 'swr'; - -export function useDatasource (id: number) { - const { data: datasource, ...rest } = useSWR(`api.datasource.${id}`, () => getDatasource(id)); - - return { datasource, ...rest }; -} diff --git a/frontend/app/src/components/datasource/no-datasource-placeholder.tsx b/frontend/app/src/components/datasource/no-datasource-placeholder.tsx new file mode 100644 index 000000000..b01f64f03 --- /dev/null +++ b/frontend/app/src/components/datasource/no-datasource-placeholder.tsx @@ -0,0 +1,9 @@ +export function NoDatasourcePlaceholder () { + return ( +
    + + Empty Data Sources list + +
    + ); +} diff --git a/frontend/app/src/components/datasource/schema.test.ts b/frontend/app/src/components/datasource/schema.test.ts deleted file mode 100644 index 3b5a2fd44..000000000 --- a/frontend/app/src/components/datasource/schema.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createDatasourceBaseSchema } from '@/components/datasource/schema'; - -test('datasource name should not be empty', () => { - expect(createDatasourceBaseSchema.safeParse({ - name: '', - description: '', - build_kg_index: false, - llm_id: null, - }).success).toBe(false); - - expect(createDatasourceBaseSchema.safeParse({ - name: ' \t\n', - description: '', - build_kg_index: false, - llm_id: null, - }).success).toBe(false); - - expect(createDatasourceBaseSchema.safeParse({ - name: 'a', - description: '', - build_kg_index: false, - llm_id: null, - }).success).toBe(true); -}); diff --git a/frontend/app/src/components/datasource/schema.ts b/frontend/app/src/components/datasource/schema.ts deleted file mode 100644 index 850ac3616..000000000 --- a/frontend/app/src/components/datasource/schema.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { DeprecatedBaseCreateDatasourceParams } from '@/api/datasources'; -import { z, type ZodType } from 'zod'; - -export const createDatasourceBaseSchema = z.object({ - name: z.string().trim().min(1, 'Must not blank'), - description: z.string(), - build_kg_index: z.boolean(), - llm_id: z.number().nullable(), -}) satisfies ZodType; diff --git a/frontend/app/src/components/datasource/types.ts b/frontend/app/src/components/datasource/types.ts deleted file mode 100644 index da1b8f66b..000000000 --- a/frontend/app/src/components/datasource/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -const types = ['file', 'web-sitemap', 'web-single-page'] as const; -export type DatasourceType = typeof types[number]; - -export function isDatasourceType (value: string): value is DatasourceType { - return types.includes(value as any); -} \ No newline at end of file diff --git a/frontend/app/src/components/datasource/update-datasource-form.tsx b/frontend/app/src/components/datasource/update-datasource-form.tsx new file mode 100644 index 000000000..a92b2e227 --- /dev/null +++ b/frontend/app/src/components/datasource/update-datasource-form.tsx @@ -0,0 +1,44 @@ +import { type Datasource, updateDatasource } from '@/api/datasources'; +import { FormInput } from '@/components/form/control-widget'; +import { FormFieldBasicLayout } from '@/components/form/field-layout'; +import { handleSubmitHelper } from '@/components/form/utils'; +import { Button } from '@/components/ui/button'; +import { Form } from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +export function UpdateDatasourceForm ({ knowledgeBaseId, datasource, onUpdated }: { knowledgeBaseId: number, datasource: Datasource, onUpdated?: () => void }) { + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: datasource.name, + }, + }); + + const handleSubmit = handleSubmitHelper(form, async data => { + await updateDatasource(knowledgeBaseId, datasource.id, data); + onUpdated?.(); + }); + + return ( +
    + + + + + +
    + + ); +} + +interface UpdateDatasourceFormParams { + name: string; +} + +const schema = z.object({ + name: z.string().min(1, 'Must not empty'), +}); \ No newline at end of file diff --git a/frontend/app/src/components/documents/DocumentDeprecationAlert.tsx b/frontend/app/src/components/documents/DocumentDeprecationAlert.tsx deleted file mode 100644 index f807e6607..000000000 --- a/frontend/app/src/components/documents/DocumentDeprecationAlert.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { AlertCircleIcon } from 'lucide-react'; - -export function DocumentDeprecationAlert () { - return ( - - - - Bare Documents management was deprecated. - - - TiDB.ai now uses Knowledge Base to manage documents. This page will be removed soon. - - - ); -} \ No newline at end of file diff --git a/frontend/app/src/components/documents/documents-table-filters.tsx b/frontend/app/src/components/documents/documents-table-filters.tsx index 1d8aedf22..a63c6298b 100644 --- a/frontend/app/src/components/documents/documents-table-filters.tsx +++ b/frontend/app/src/components/documents/documents-table-filters.tsx @@ -22,23 +22,25 @@ export function DocumentsTableFilters ({ onFilterChange }: { table: ReactTable -
    - ( - - - - - - - )} - /> - - - - Advanced Filters - + + +
    + ( + + + + + + + )} + /> + + Advanced Filters + + +
    - Select Index Status...} /> + Select Index Status...} /> {indexStatuses.map(indexStatus => ( @@ -170,9 +172,9 @@ export function DocumentsTableFilters ({ onFilterChange }: { table: ReactTable
    - - - + + + ); } \ No newline at end of file diff --git a/frontend/app/src/components/documents/documents-table.tsx b/frontend/app/src/components/documents/documents-table.tsx index ac1d60253..c62980003 100644 --- a/frontend/app/src/components/documents/documents-table.tsx +++ b/frontend/app/src/components/documents/documents-table.tsx @@ -8,10 +8,12 @@ import { DataTableRemote } from '@/components/data-table-remote'; import { DocumentPreviewDialog } from '@/components/document-viewer'; import { DocumentsTableFilters } from '@/components/documents/documents-table-filters'; import { DocumentChunksTable } from '@/components/knowledge-base/document-chunks-table'; +import { NextLink } from '@/components/nextjs/NextLink'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import type { CellContext, ColumnDef } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/table-core'; +import { UploadIcon } from 'lucide-react'; import { useMemo, useState } from 'react'; const helper = createColumnHelper(); @@ -28,13 +30,14 @@ const href = (cell: CellContext) => [ helper.accessor('id', { cell: mono }), helper.accessor('knowledge_base', { cell: ctx => }), - helper.display({ id: 'name', header: 'name', cell: ({ row }) => - + helper.display({ + id: 'name', header: 'name', cell: ({ row }) => + , }), helper.accessor('source_uri', { cell: href }), helper.accessor('mime_type', { cell: mono }), @@ -47,8 +50,8 @@ const getColumns = (kbId?: number) => [ header: 'action', cell: ({ row }) => (kbId ?? row.original.knowledge_base?.id) != null && ( - - @@ -70,23 +73,28 @@ const getColumns = (kbId?: number) => [ }), ] as ColumnDef[]; -export function DocumentsTable ({ knowledgeBaseId }: { knowledgeBaseId?: number }) { +export function DocumentsTable ({ knowledgeBaseId }: { knowledgeBaseId: number }) { const [filters, setFilters] = useState({}); const columns = useMemo(() => { - if (knowledgeBaseId != null) { - const columns = [...getColumns(knowledgeBaseId)]; - columns.splice(1, 1); - return columns; - } else { - return getColumns(knowledgeBaseId); - } + const columns = [...getColumns(knowledgeBaseId)]; + columns.splice(1, 1); + return columns; }, [knowledgeBaseId]); return ( ( - +
    + + + Upload documents + + +
    ))} columns={columns} apiKey={knowledgeBaseId != null ? `api.datasource.${knowledgeBaseId}.documents` : 'api.documents.list'} diff --git a/frontend/app/src/components/documents/hooks.ts b/frontend/app/src/components/documents/hooks.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/app/src/components/embedding-models/EmbeddingModelInfo.tsx b/frontend/app/src/components/embedding-models/EmbeddingModelInfo.tsx index 6d869f9f3..fb59f307d 100644 --- a/frontend/app/src/components/embedding-models/EmbeddingModelInfo.tsx +++ b/frontend/app/src/components/embedding-models/EmbeddingModelInfo.tsx @@ -3,7 +3,7 @@ import { useEmbeddingModel } from '@/components/embedding-models/hooks'; import { ModelComponentInfo } from '@/components/model-component-info'; -export function EmbeddingModelInfo ({ className, reverse = false, detailed = false, id }: { className?: string, reverse?: boolean, detailed?: boolean, id: number | undefined | null }) { +export function EmbeddingModelInfo ({ className, id }: { className?: string, id: number | undefined | null }) { const { embeddingModel, isLoading } = useEmbeddingModel(id); return `/embedding-models/${embeddingModel.id}`} isLoading={isLoading} - reverse={reverse} - detailed={detailed} defaultName="Default Embedding Model" />; } diff --git a/frontend/app/src/components/embedding-models/EmbeddingModelsTable.tsx b/frontend/app/src/components/embedding-models/EmbeddingModelsTable.tsx index 148d460a9..747f8efc0 100644 --- a/frontend/app/src/components/embedding-models/EmbeddingModelsTable.tsx +++ b/frontend/app/src/components/embedding-models/EmbeddingModelsTable.tsx @@ -50,10 +50,9 @@ const columns: ColumnDef[] = [ cell: ({ row }) => { const { model, provider } = row.original; return ( - - {provider} - {model} - + <> + {provider}:{model} + ); }, }), diff --git a/frontend/app/src/components/embedding-models/hooks.tsx b/frontend/app/src/components/embedding-models/hooks.tsx index 4ba7682cc..ca7d55e06 100644 --- a/frontend/app/src/components/embedding-models/hooks.tsx +++ b/frontend/app/src/components/embedding-models/hooks.tsx @@ -1,7 +1,16 @@ -import { getEmbeddingModelById } from '@/api/embedding-models'; +import { listEmbeddingModels } from '@/api/embedding-models'; +import { listAllHelper } from '@/lib/request'; import useSWR from 'swr'; +export function useAllEmbeddingModels (flag = true) { + return useSWR(flag && 'api.embedding-models.list-all', () => listAllHelper(listEmbeddingModels, 'id')); +} + export function useEmbeddingModel (id: number | null | undefined) { - const { data: embeddingModel, ...rest } = useSWR(id == null ? null : `api.embedding-model.get?id=${id}`, () => getEmbeddingModelById(id as number)); - return { embeddingModel, ...rest }; + const { data, mutate, ...rest } = useAllEmbeddingModels(id != null); + + return { + embeddingModel: data?.find(embeddingModel => embeddingModel.id === id), + ...rest, + }; } diff --git a/frontend/app/src/components/form/biz.tsx b/frontend/app/src/components/form/biz.tsx index 90e30eea6..066671458 100644 --- a/frontend/app/src/components/form/biz.tsx +++ b/frontend/app/src/components/form/biz.tsx @@ -1,35 +1,35 @@ -import { type EmbeddingModel, listEmbeddingModels } from '@/api/embedding-models'; -import { type KnowledgeBaseSummary, listKnowledgeBases } from '@/api/knowledge-base'; -import { listLlms, type LLM } from '@/api/llms'; +import { type EmbeddingModel } from '@/api/embedding-models'; +import { type KnowledgeBaseSummary } from '@/api/knowledge-base'; +import { type LLM } from '@/api/llms'; import type { ProviderOption } from '@/api/providers'; -import { listRerankers, type Reranker } from '@/api/rerankers'; +import { type Reranker } from '@/api/rerankers'; import { CreateEmbeddingModelForm } from '@/components/embedding-models/CreateEmbeddingModelForm'; -import { FormCombobox, type FormComboboxConfig, type FormComboboxProps, FormSelect, type FormSelectConfig, type FormSelectProps } from '@/components/form/control-widget'; -import { KBInfo } from '@/components/knowledge-base/KBInfo'; +import { useAllEmbeddingModels } from '@/components/embedding-models/hooks'; +import { FormCombobox, type FormComboboxConfig, type FormComboboxProps } from '@/components/form/control-widget'; +import { useAllKnowledgeBases } from '@/components/knowledge-base/hooks'; import { CreateLLMForm } from '@/components/llm/CreateLLMForm'; +import { useAllLlms } from '@/components/llm/hooks'; import { ManagedDialog } from '@/components/managed-dialog'; import { ManagedPanelContext } from '@/components/managed-panel'; import { CreateRerankerForm } from '@/components/reranker/CreateRerankerForm'; -import { RerankerInfo } from '@/components/reranker/RerankerInfo'; +import { useAllRerankers } from '@/components/reranker/hooks'; import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { PlusIcon } from 'lucide-react'; +import { AlertTriangleIcon, DotIcon, PlusIcon } from 'lucide-react'; import { forwardRef } from 'react'; -import useSWR from 'swr'; export const EmbeddingModelSelect = forwardRef & { reverse?: boolean }>(({ reverse = true, ...props }, ref) => { - // TODO - const { data: embeddingModels, isLoading, mutate, error } = useSWR('api.embedding-models.list-all', () => listEmbeddingModels({ size: 100 })); + const { data: embeddingModels, isLoading, mutate, error } = useAllEmbeddingModels(); return ( [option.name, option.provider, option.model], loading: isLoading, error, - renderValue: option => ({option.name} [{option.vector_dimension}]), + renderValue: option => ({option.name} [{option.vector_dimension}]), renderOption: option => (
    {option.name}
    @@ -78,14 +78,14 @@ export const EmbeddingModelSelect = forwardRef & { reverse?: boolean }>(({ reverse = true, ...props }, ref) => { - const { data: llms, isLoading, mutate, error } = useSWR('api.llms.list-all', () => listLlms({ size: 100 })); + const { data: llms, isLoading, mutate, error } = useAllLlms(); return ( ({option.name}), @@ -138,14 +138,14 @@ export const LLMSelect = forwardRef & { r LLMSelect.displayName = 'LLMSelect'; export const RerankerSelect = forwardRef & { reverse?: boolean }>(({ reverse = true, ...props }, ref) => { - const { data: rerankers, mutate, isLoading, error } = useSWR('api.rerankers.list-all', () => listRerankers({ size: 100 })); + const { data: rerankers, mutate, isLoading, error } = useAllRerankers(); return ( [option.name, option.provider, option.model], loading: isLoading, error, @@ -197,7 +197,7 @@ export const RerankerSelect = forwardRef RerankerSelect.displayName = 'RerankerSelect'; -export interface ProviderSelectProps extends Omit { +export interface ProviderSelectProps extends Omit { options: ProviderOption[] | undefined; isLoading: boolean; error: unknown; @@ -207,22 +207,24 @@ export const ProviderSelect = forwardRef(({ options, isLoading, error, ...props }, ref) => { return ( - [option.provider, option.provider_description ?? '', option.provider_display_name ?? ''], loading: isLoading, error, renderOption: option => ( - <> -
    {option.provider_display_name ?? option.provider}
    +
    +
    {option.provider_display_name ?? option.provider}
    {option.provider_description &&
    {option.provider_description}
    } - +
    ), itemClassName: 'space-y-1', renderValue: option => option.provider_display_name ?? option.provider, key: 'provider', - } satisfies FormSelectConfig} + } satisfies FormComboboxConfig} + contentWidth="anchor" {...props} /> ); @@ -230,22 +232,56 @@ export const ProviderSelect = forwardRef(({ ProviderSelect.displayName = 'ProviderSelect'; -export const KBSelect = forwardRef & { reverse?: boolean }>(({ reverse = true, ...props }, ref) => { - const { data: kbs, isLoading, error } = useSWR('api.knowledge-bases.list-all', () => listKnowledgeBases({ size: 100 })); +export const KBSelect = forwardRef & { reverse?: boolean }>(({ reverse = true, ...props }, ref) => { + const { data: kbs, isLoading, error } = useAllKnowledgeBases(); return ( - [String(option.id), option.name, option.description], loading: isLoading, error, - renderValue: option => (), - renderOption: option => (), + renderValue: option => ( +
    + {option.name} +
    + + {(option.documents_total ?? 0) || <> no} documents + + + + {(option.data_sources_total ?? 0) || <> no} data sources + +
    +
    + ), + renderOption: option => ( +
    +
    + + {option.name} + +
    +
    + + {(option.documents_total ?? 0) || <> no} documents + + + + {(option.data_sources_total ?? 0) || <> no} data sources + +
    +
    + {option.description} +
    +
    + ), key: 'id', - } satisfies FormSelectConfig} + } satisfies FormComboboxConfig} /> ); }); diff --git a/frontend/app/src/components/form/control-widget.tsx b/frontend/app/src/components/form/control-widget.tsx index 232a6847f..e0d7ad679 100644 --- a/frontend/app/src/components/form/control-widget.tsx +++ b/frontend/app/src/components/form/control-widget.tsx @@ -140,9 +140,10 @@ export interface FormComboboxProps extends FormControlWidgetProps { children?: ReactElement; placeholder?: string; config: FormComboboxConfig; + contentWidth?: 'anchor'; } -export const FormCombobox = forwardRef(({ config, placeholder, value, onChange, name, disabled, children, ...props }, ref) => { +export const FormCombobox = forwardRef(({ config, placeholder, value, onChange, name, disabled, children, contentWidth, ...props }, ref) => { const [open, setOpen] = useState(false); const isConfigReady = !config.loading && !config.error; const current = config.options.find(option => option[config.key] === value); @@ -190,7 +191,7 @@ export const FormCombobox = forwardRef(({ config, placeh
    - + @@ -209,7 +210,7 @@ export const FormCombobox = forwardRef(({ config, placeh item.split(/\s+/))} className={cn('group', config.itemClassName)} onSelect={value => { const item = config.options.find(option => String(option[config.key]) === value); diff --git a/frontend/app/src/components/knowledge-base/KBInfo.tsx b/frontend/app/src/components/knowledge-base/KBInfo.tsx deleted file mode 100644 index e9bb2cf62..000000000 --- a/frontend/app/src/components/knowledge-base/KBInfo.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'use client'; - -import { useKnowledgeBase } from '@/components/knowledge-base/hooks'; - -export function KBInfo ({ className, detailed = false, id }: { className?: string, detailed?: boolean, id: number | undefined | null }) { - const { knowledgeBase, isLoading } = useKnowledgeBase(id); - - return ( - {knowledgeBase?.name} - ); -} diff --git a/frontend/app/src/components/knowledge-base/create-knowledge-base-form.tsx b/frontend/app/src/components/knowledge-base/create-knowledge-base-form.tsx index 4a4455df9..12a5c1155 100644 --- a/frontend/app/src/components/knowledge-base/create-knowledge-base-form.tsx +++ b/frontend/app/src/components/knowledge-base/create-knowledge-base-form.tsx @@ -1,11 +1,10 @@ -import { uploadFiles } from '@/api/datasources'; import { createKnowledgeBase } from '@/api/knowledge-base'; import { EmbeddingModelSelect, LLMSelect } from '@/components/form/biz'; import { FormInput, FormTextarea } from '@/components/form/control-widget'; import { FormFieldBasicLayout } from '@/components/form/field-layout'; import { handleSubmitHelper } from '@/components/form/utils'; -import { createDatasourceSchema, FormCreateDataSources } from '@/components/knowledge-base/form-create-data-sources'; import { FormIndexMethods } from '@/components/knowledge-base/form-index-methods'; +import { mutateKnowledgeBases } from '@/components/knowledge-base/hooks'; import { Button } from '@/components/ui/button'; import { Form } from '@/components/ui/form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -26,58 +25,26 @@ export function CreateKnowledgeBaseForm ({}: {}) { name: '', description: '', index_methods: ['vector'], - data_sources: [ - { name: 'Default Datasource', description: '', data_source_type: 'file', files: [] }, - ], + data_sources: [], }, }); const handleSubmit = handleSubmitHelper(form, async (data) => { - const dataSources = await Promise.all(data.data_sources.map(async (ds) => { - switch (ds.data_source_type) { - case 'file': { - const { files, ...rest } = ds; - const uploadedFiles = await uploadFiles(ds.files); - return { - ...rest, - config: uploadedFiles.map(f => ({ - file_id: f.id, - file_name: f.name, - })), - }; - } - case 'web_single_page': { - const { urls, ...rest } = ds; - - return { - ...rest, - config: { urls }, - }; - } - - case 'web_sitemap': - const { url, ...rest } = ds; - - return { - ...rest, - config: { url }, - }; - } - })); const kb = await createKnowledgeBase({ ...data, - data_sources: dataSources, + data_sources: [], }); startTransition(() => { - router.push(`/knowledge-bases/${kb.id}`); + router.push(`/knowledge-bases/${kb.id}/data-sources`); + mutateKnowledgeBases(); }); }); return (
    - + @@ -90,7 +57,6 @@ export function CreateKnowledgeBaseForm ({}: {}) { - @@ -106,5 +72,5 @@ const createKnowledgeBaseParamsSchema = z.object({ index_methods: z.enum(['knowledge_graph', 'vector']).array(), llm_id: z.number().nullable().optional(), embedding_model_id: z.number().nullable().optional(), - data_sources: createDatasourceSchema.array(), // use external form + data_sources: z.never().array().length(0), // use external form }); diff --git a/frontend/app/src/components/knowledge-base/datasource-details.tsx b/frontend/app/src/components/knowledge-base/datasource-details.tsx deleted file mode 100644 index 4b94735ec..000000000 --- a/frontend/app/src/components/knowledge-base/datasource-details.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client'; - -import { useKnowledgeBaseDatasource } from '@/components/knowledge-base/hooks'; -import { OptionDetail } from '@/components/option-detail'; -import { Badge } from '@/components/ui/badge'; - -export function KnowledgeBaseDatasourceDetails ({ id }: { id: number }) { - return ( - <> - - - - ); -} - -function KnowledgeBaseDatasourceFields ({ id }: { id: number }) { - const datasource = useKnowledgeBaseDatasource(id); - return ( - - ); -} - -function KnowledgeBaseDatasourceUploadFiles ({ id }: { id: number }) { - const datasource = useKnowledgeBaseDatasource(id); - - if (datasource?.data_source_type !== 'file' || datasource.config.length === 0) { - return null; - } - return ( -
    -
    Files
    -
    - {datasource.config.map(file => ( - - - {file.file_name} - - #{file.file_id} - - ))} -
    -
    - ); -} diff --git a/frontend/app/src/components/knowledge-base/form-create-data-sources.tsx b/frontend/app/src/components/knowledge-base/form-create-data-sources.tsx deleted file mode 100644 index 479f259a9..000000000 --- a/frontend/app/src/components/knowledge-base/form-create-data-sources.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import type { BaseCreateDatasourceParams, CreateDatasourceSpecParams } from '@/api/datasources'; -import type { CreateKnowledgeBaseParams } from '@/api/knowledge-base'; -import { FormInput } from '@/components/form/control-widget'; -import { FormFieldBasicLayout, FormPrimitiveArrayFieldBasicLayout } from '@/components/form/field-layout'; -import { FilesInput } from '@/components/form/widgets/FilesInput'; -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; -import { Button } from '@/components/ui/button'; -import { FormItem, FormLabel, FormMessage } from '@/components/ui/form'; -import { FormArrayField } from '@/components/ui/form.ext'; -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { cn } from '@/lib/utils'; -import { zodFile } from '@/lib/zod'; -import { PlusIcon } from 'lucide-react'; -import { useId } from 'react'; -import { useFormContext } from 'react-hook-form'; -import { z } from 'zod'; - -export const createDatasourceSchema = z.object({ - name: z.string().trim().min(1, 'Must not blank'), - description: z.string(), -}).and(z.discriminatedUnion('data_source_type', [ - z.object({ - data_source_type: z.literal('file'), - files: zodFile().array().min(1), - }), - z.object({ - data_source_type: z.literal('web_single_page'), - urls: z.string().url().array().min(1), - }), - z.object({ - data_source_type: z.literal('web_sitemap'), - url: z.string().url(), - }), -])); - -export function FormCreateDataSources () { - return ( - - name="data_sources" - render={({ field: { fields, update, append, remove } }) => ( - - Datasource - - {(fields).map((field, index) => ( - - - - - - update(index, { ...switchDatasource(field, value as never) }))} - > - - - File - - - Web Single Page - - - Web Sitemap - - - - {field.data_source_type === 'file' && ( - - - - )} - {field.data_source_type === 'web_single_page' && ( - ''}> - - - )} - {field.data_source_type === 'web_sitemap' && ( - - - - )} - - - - - {fields.length > 1 && } - - - ))} - - - - - )} - />) - ; -} - -function DatasourceName ({ index }: { index: number }) { - const { watch, formState } = useFormContext(); - const errors = formState.errors.data_sources?.[index]; - const name = watch(`data_sources.${index}.name`); - - return ( - - {index + 1}. - {name || Unnamed} - - ); -} - -function switchDatasource (data: BaseCreateDatasourceParams & CreateDatasourceSpecParams, type: CreateDatasourceSpecParams['data_source_type']): BaseCreateDatasourceParams & CreateDatasourceSpecParams { - switch (type) { - case 'file': - return { - name: data.name, - data_source_type: 'file', - config: [], - }; - case 'web_single_page': - return { - name: data.name, - data_source_type: 'web_single_page', - config: { urls: [] }, - }; - case 'web_sitemap': - return { - name: data.name, - data_source_type: 'web_sitemap', - config: { url: '' }, - }; - } -} diff --git a/frontend/app/src/components/knowledge-base/hooks.ts b/frontend/app/src/components/knowledge-base/hooks.ts index 5c1d3597f..6d67e077f 100644 --- a/frontend/app/src/components/knowledge-base/hooks.ts +++ b/frontend/app/src/components/knowledge-base/hooks.ts @@ -1,33 +1,35 @@ -import { getKnowledgeBaseById, getKnowledgeGraphIndexProgress, listKnowledgeBases } from '@/api/knowledge-base'; -import { useKB } from '@/app/(main)/(admin)/knowledge-bases/[id]/context'; -import useSWR from 'swr'; - -export function useKnowledgeBase (id: number | null | undefined) { - const { data: knowledgeBase, ...rest } = useSWR(id != null && `api.knowledge-bases.get?id=${id}`, () => getKnowledgeBaseById(id!)) - return { knowledgeBase, ...rest } -} - -export function useKnowledgeBaseDatasource (id: number) { - const { data_sources } = useKB(); - - return data_sources.find(ds => ds.id === id); -} +import { listDataSources } from '@/api/datasources'; +import { getKnowledgeGraphIndexProgress, listKnowledgeBases } from '@/api/knowledge-base'; +import { listAllHelper } from '@/lib/request'; +import useSWR, { mutate } from 'swr'; export function useKnowledgeBaseIndexProgress (id: number) { const { data: progress, ...rest } = useSWR(`api.knowledge-base.${id}.index-progress`, () => getKnowledgeGraphIndexProgress(id)); return { progress, ...rest }; } -export function useKnowledgeBases (pageIndex: number, pageSize: number) { - const { data: knowledgeBases, ...rest } = useSWR(`api.knowledge-bases.list?page=${pageIndex}&size=${pageSize}`, () => listKnowledgeBases({ page: pageIndex + 1, size: pageSize }), { - revalidateOnReconnect: false, - revalidateOnFocus: false, - focusThrottleInterval: 1000, - keepPreviousData: true, - }); +export function useAllKnowledgeBases (flag = true) { + return useSWR(flag && `api.knowledge-bases.list-all`, () => listAllHelper(listKnowledgeBases, 'id')); +} + +export function useKnowledgeBase (id: number | null | undefined) { + const { data, mutate, ...rest } = useAllKnowledgeBases(id != null); return { - knowledgeBases, + knowledgeBase: data?.find(llm => llm.id === id), ...rest, }; } + +export function useAllKnowledgeBaseDataSources (kbId: number, flag = true) { + return useSWR(flag && `api.knowledge-bases.${kbId}.data-sources.list-all`, () => listAllHelper((params) => listDataSources(kbId, params), 'id')); +} + +export function mutateKnowledgeBases () { + return mutate(key => { + if (typeof key === 'string') { + return key.startsWith(`api.knowledge-bases.`); + } + return false; + }); +} \ No newline at end of file diff --git a/frontend/app/src/components/knowledge-base/knowledge-base-card.tsx b/frontend/app/src/components/knowledge-base/knowledge-base-card.tsx index d4e740a6e..a0c14995c 100644 --- a/frontend/app/src/components/knowledge-base/knowledge-base-card.tsx +++ b/frontend/app/src/components/knowledge-base/knowledge-base-card.tsx @@ -6,7 +6,8 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader } from '@/co import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { Separator } from '@/components/ui/separator'; import { Skeleton } from '@/components/ui/skeleton'; -import { Book, Ellipsis } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { AlertTriangleIcon, Book, Ellipsis } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { ReactNode, startTransition, useState } from 'react'; import { mutate } from 'swr'; @@ -40,7 +41,7 @@ export function KnowledgeBaseCard ({ knowledgeBase, children }: { knowledgeBase: }; return ( - +
    @@ -51,7 +52,7 @@ export function KnowledgeBaseCard ({ knowledgeBase, children }: { knowledgeBase:
    {knowledgeBase.documents_total ?? 0} documents ยท - {knowledgeBase.data_sources_total ?? 0} data sources + {(knowledgeBase.data_sources_total ?? 0) || <> No} data sources
    @@ -73,7 +74,7 @@ export function KnowledgeBaseCard ({ knowledgeBase, children }: { knowledgeBase: - event.stopPropagation()}> + event.stopPropagation()}> Settings diff --git a/frontend/app/src/components/knowledge-base/knowledge-base-index.tsx b/frontend/app/src/components/knowledge-base/knowledge-base-index.tsx index bc55c2439..c22a3224e 100644 --- a/frontend/app/src/components/knowledge-base/knowledge-base-index.tsx +++ b/frontend/app/src/components/knowledge-base/knowledge-base-index.tsx @@ -2,7 +2,6 @@ import { type DatasourceKgIndexError, type DatasourceVectorIndexError } from '@/api/datasources'; import { listKnowledgeBaseKgIndexErrors, listKnowledgeBaseVectorIndexErrors, retryKnowledgeBaseAllFailedTasks } from '@/api/knowledge-base'; -import { useKB } from '@/app/(main)/(admin)/knowledge-bases/[id]/context'; import { link } from '@/components/cells/link'; import { IndexProgressChart, IndexProgressChartPlaceholder } from '@/components/charts/IndexProgressChart'; import { TotalCard } from '@/components/charts/TotalCard'; @@ -17,7 +16,6 @@ import { ArrowRightIcon, FileTextIcon, PuzzleIcon, RouteIcon } from 'lucide-reac import Link from 'next/link'; export function KnowledgeBaseIndexProgress ({ id }: { id: number }) { - const { index_methods } = useKB(); const { progress, isLoading } = useKnowledgeBaseIndexProgress(id); return ( diff --git a/frontend/app/src/components/knowledge-base/knowledge-base-settings-form.tsx b/frontend/app/src/components/knowledge-base/knowledge-base-settings-form.tsx index a1ef24724..75109a913 100644 --- a/frontend/app/src/components/knowledge-base/knowledge-base-settings-form.tsx +++ b/frontend/app/src/components/knowledge-base/knowledge-base-settings-form.tsx @@ -4,6 +4,7 @@ import { type KnowledgeBase, type KnowledgeBaseIndexMethod, updateKnowledgeBase import { EmbeddingModelSelect, LLMSelect } from '@/components/form/biz'; import { FormInput, FormSwitch, FormTextarea } from '@/components/form/control-widget'; import { FormFieldBasicLayout, FormFieldContainedLayout } from '@/components/form/field-layout'; +import { mutateKnowledgeBases } from '@/components/knowledge-base/hooks'; import { fieldAccessor, GeneralSettingsField, type GeneralSettingsFieldAccessor, GeneralSettingsForm, shallowPick } from '@/components/settings-form'; import type { KeyOfType } from '@/lib/typing-utils'; import { format } from 'date-fns'; @@ -26,6 +27,7 @@ export function KnowledgeBaseSettingsForm ({ knowledgeBase }: { knowledgeBase: K await updateKnowledgeBase(knowledgeBase.id, partial); startTransition(() => { router.refresh(); + mutateKnowledgeBases(); }); } else { throw new Error(`${path.map(p => String(p)).join('.')} is not updatable currently.`); diff --git a/frontend/app/src/components/llm/LLMsTable.tsx b/frontend/app/src/components/llm/LLMsTable.tsx index 4d6bd03ab..67f916d6a 100644 --- a/frontend/app/src/components/llm/LLMsTable.tsx +++ b/frontend/app/src/components/llm/LLMsTable.tsx @@ -49,10 +49,9 @@ const columns: ColumnDef[] = [ cell: ({ row }) => { const { model, provider } = row.original; return ( - - {provider} - {model} - + <> + {provider}:{model} + ); }, }), diff --git a/frontend/app/src/components/llm/LlmInfo.tsx b/frontend/app/src/components/llm/LlmInfo.tsx index ec15b4c1b..37b385858 100644 --- a/frontend/app/src/components/llm/LlmInfo.tsx +++ b/frontend/app/src/components/llm/LlmInfo.tsx @@ -3,7 +3,7 @@ import { useLlm } from '@/components/llm/hooks'; import { ModelComponentInfo } from '@/components/model-component-info'; -export function LlmInfo ({ className, reverse = false, detailed = false, id }: { className?: string, reverse?: boolean, detailed?: boolean, id: number | undefined | null }) { +export function LlmInfo ({ className, id }: { className?: string, id: number | undefined | null }) { const { llm, isLoading } = useLlm(id); return `/llms/${llm.id}`} isLoading={isLoading} - reverse={reverse} - detailed={detailed} defaultName="Default LLM" />; } diff --git a/frontend/app/src/components/llm/hooks.ts b/frontend/app/src/components/llm/hooks.ts index fa1a8d03b..9fe596bb2 100644 --- a/frontend/app/src/components/llm/hooks.ts +++ b/frontend/app/src/components/llm/hooks.ts @@ -1,7 +1,16 @@ -import { getLlm } from '@/api/llms'; +import { listLlms } from '@/api/llms'; +import { listAllHelper } from '@/lib/request'; import useSWR from 'swr'; +export function useAllLlms (flag: boolean = true) { + return useSWR(flag && 'api.llms.list-all', () => listAllHelper(listLlms, 'id')); +} + export function useLlm (id: number | null | undefined) { - const { data: llm, ...rest } = useSWR(id == null ? null : `api.llms.get?id=${id}`, () => getLlm(id as number)); - return { llm, ...rest }; + const { data, mutate, ...rest } = useAllLlms(id != null); + + return { + llm: data?.find(llm => llm.id === id), + ...rest, + }; } diff --git a/frontend/app/src/components/model-component-info.tsx b/frontend/app/src/components/model-component-info.tsx index d489cb0e5..9b2d57ebe 100644 --- a/frontend/app/src/components/model-component-info.tsx +++ b/frontend/app/src/components/model-component-info.tsx @@ -1,4 +1,3 @@ -import { Badge, badgeVariants } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; import { Loader2Icon } from 'lucide-react'; import Link from 'next/link'; @@ -14,26 +13,25 @@ export interface ModelComponentInfoProps { className?: string; isLoading?: boolean; model: Model | null | undefined; - detailed?: boolean; - reverse?: boolean; url: (model: Model) => string; defaultName?: string; } -export function ModelComponentInfo ({ className, reverse = false, detailed = false, isLoading = false, model, url, defaultName }: ModelComponentInfoProps) { - +export function ModelComponentInfo ({ className, isLoading = false, model, url, defaultName }: ModelComponentInfoProps) { if (isLoading) { return ; } if (!model) { - return defaultName && {defaultName}; + return defaultName && {defaultName}; } return ( - - {detailed && {model.provider ?? 'unknown-provider'}:{model.model}} - {model.name} + + {model.name} + + {model.provider}:{model.model} + ); } diff --git a/frontend/app/src/components/reranker/RerankerInfo.tsx b/frontend/app/src/components/reranker/RerankerInfo.tsx index f054e2a9e..36e80a70c 100644 --- a/frontend/app/src/components/reranker/RerankerInfo.tsx +++ b/frontend/app/src/components/reranker/RerankerInfo.tsx @@ -3,7 +3,7 @@ import { ModelComponentInfo } from '@/components/model-component-info'; import { useReranker } from '@/components/reranker/hooks'; -export function RerankerInfo ({ className, reverse = false, detailed = false, id }: { className?: string, reverse?: boolean, detailed?: boolean, id: number | undefined | null }) { +export function RerankerInfo ({ className, id }: { className?: string, id: number | undefined | null }) { const { reranker, isLoading } = useReranker(id); return `/reranker-models/${reranker.id}`} isLoading={isLoading} - reverse={reverse} - detailed={detailed} defaultName="Default Reranker Model" />; } diff --git a/frontend/app/src/components/reranker/RerankerModelsTable.tsx b/frontend/app/src/components/reranker/RerankerModelsTable.tsx index 419ce44d8..57e0bd773 100644 --- a/frontend/app/src/components/reranker/RerankerModelsTable.tsx +++ b/frontend/app/src/components/reranker/RerankerModelsTable.tsx @@ -48,10 +48,9 @@ const columns: ColumnDef[] = [ cell: ({ row }) => { const { model, provider } = row.original; return ( - - {provider} - {model} - + <> + {provider}:{model} + ); }, }), diff --git a/frontend/app/src/components/reranker/hooks.ts b/frontend/app/src/components/reranker/hooks.ts index 19fa497de..96a956f21 100644 --- a/frontend/app/src/components/reranker/hooks.ts +++ b/frontend/app/src/components/reranker/hooks.ts @@ -1,7 +1,16 @@ -import { getReranker } from '@/api/rerankers'; +import { listRerankers } from '@/api/rerankers'; +import { listAllHelper } from '@/lib/request'; import useSWR from 'swr'; +export function useAllRerankers (flag = true) { + return useSWR(flag && 'api.rerankers.list-all', () => listAllHelper(listRerankers, 'id')); +} + export function useReranker (id: number | null | undefined) { - const { data: reranker, ...rest } = useSWR(id == null ? null : `api.rerankers.get?id=${id}`, () => getReranker(id as number)); - return { reranker, ...rest }; + const { data, mutate, ...rest } = useAllRerankers(id != null); + + return { + reranker: data?.find(reranker => reranker.id === id), + ...rest, + }; } diff --git a/frontend/app/src/lib/request/index.ts b/frontend/app/src/lib/request/index.ts index 3e04acaef..7457e4ba5 100644 --- a/frontend/app/src/lib/request/index.ts +++ b/frontend/app/src/lib/request/index.ts @@ -5,3 +5,4 @@ export * from './errors'; export * from './params'; export { type Page, type PageParams, zodPage } from '../zod'; export * from './url'; +export * from './list-all-helper'; diff --git a/frontend/app/src/lib/request/list-all-helper.ts b/frontend/app/src/lib/request/list-all-helper.ts new file mode 100644 index 000000000..d0a3f4178 --- /dev/null +++ b/frontend/app/src/lib/request/list-all-helper.ts @@ -0,0 +1,30 @@ +import type { Page, PageParams } from '@/lib/zod'; + +export async function listAllHelper (api: (params: PageParams) => Promise>, idField: keyof T) { + let page = 1; + const chunks: Page[] = []; + + while (true) { + const current = await api({ page, size: 100 }); + chunks.push(current); + if (page < current.pages) { + page += 1; + } else { + break; + } + } + + const idSet = new Set(); + const result: T[] = []; + + for (const chunk of chunks) { + for (const item of chunk.items) { + if (!idSet.has(item[idField])) { + idSet.add(item[idField]); + result.push(item); + } + } + } + + return result; +} \ No newline at end of file