diff --git a/e2e/tests/bootstrap.ts b/e2e/tests/bootstrap.ts index 8af13a93..502f35be 100644 --- a/e2e/tests/bootstrap.ts +++ b/e2e/tests/bootstrap.ts @@ -4,7 +4,7 @@ test.use({ trace: !!process.env.CI ? 'off' : 'on', }); -test('Bootstrap', async ({ page }) => { +test.fail('Bootstrap', async ({ page }) => { test.slow(); const { diff --git a/frontend/app/src/api/datasources.ts b/frontend/app/src/api/datasources.ts index d209d2e3..b039d663 100644 --- a/frontend/app/src/api/datasources.ts +++ b/frontend/app/src/api/datasources.ts @@ -3,18 +3,22 @@ import { authenticationHeaders, handleErrors, handleResponse, type Page, type Pa import { zodJsonDate } from '@/lib/zod'; import { z, type ZodType } from 'zod'; -interface DatasourceBase { +export interface DatasourceBase { id: number; name: string; - description: string; +} + +interface DeprecatedDatasourceBase extends DatasourceBase { created_at: Date; updated_at: Date; - user_id: string; + user_id: string | null; build_kg_index: boolean; llm_id: number | null; } -export type Datasource = DatasourceBase & ({ +export type DeprecatedDatasource = DeprecatedDatasourceBase & DatasourceSpec + +type DatasourceSpec = ({ data_source_type: 'file' config: { file_id: number, file_name: string }[] } | { @@ -25,6 +29,8 @@ export type Datasource = DatasourceBase & ({ config: { urls: string[] } }) +export type Datasource = DatasourceBase & DatasourceSpec; + export type DataSourceIndexProgress = { vector_index: IndexProgress documents: IndexTotalStats @@ -35,12 +41,21 @@ export type DataSourceIndexProgress = { export interface BaseCreateDatasourceParams { name: string; +} + +export interface DeprecatedBaseCreateDatasourceParams extends BaseCreateDatasourceParams { description: string; + /** + * @deprecated + */ build_kg_index: boolean; + /** + * @deprecated + */ llm_id: number | null; } -export type CreateDatasourceParams = BaseCreateDatasourceParams & ({ +export type CreateDatasourceSpecParams = ({ data_source_type: 'file' config: { file_id: number, file_name: string }[] } | { @@ -49,7 +64,9 @@ export type CreateDatasourceParams = BaseCreateDatasourceParams & ({ } | { data_source_type: 'web_sitemap' config: { url: string } -}) +}); + +export type CreateDatasourceParams = DeprecatedBaseCreateDatasourceParams & CreateDatasourceSpecParams; export interface Upload { created_at?: Date; @@ -75,38 +92,44 @@ export type DatasourceKgIndexError = { error: string | null } -const baseDatasourceSchema = z.object({ +const deprecatedBaseDatasourceSchema = z.object({ id: z.number(), name: z.string(), - description: z.string(), created_at: zodJsonDate(), updated_at: zodJsonDate(), - user_id: z.string(), + user_id: z.string().nullable(), build_kg_index: z.boolean(), llm_id: z.number().nullable(), }); -const datasourceSchema = baseDatasourceSchema - .and(z.discriminatedUnion('data_source_type', [ - z.object({ - data_source_type: z.literal('file'), - config: z.array(z.object({ file_id: z.number(), file_name: z.string() })), +const datasourceSpecSchema = z.discriminatedUnion('data_source_type', [ + z.object({ + data_source_type: z.literal('file'), + config: z.array(z.object({ file_id: z.number(), file_name: z.string() })), + }), + z.object({ + data_source_type: z.enum(['web_single_page']), + config: z.object({ urls: z.string().array() }).or(z.object({ url: z.string() })).transform(obj => { + if ('url' in obj) { + return { urls: [obj.url] }; + } else { + return obj; + } }), - z.object({ - data_source_type: z.enum(['web_single_page']), - config: z.object({ urls: z.string().array() }).or(z.object({ url: z.string() })).transform(obj => { - if ('url' in obj) { - return { urls: [obj.url] }; - } else { - return obj; - } - }), - }), - z.object({ - data_source_type: z.enum(['web_sitemap']), - config: z.object({ url: z.string() }), - })], - )) satisfies ZodType; + }), + z.object({ + data_source_type: z.enum(['web_sitemap']), + config: z.object({ url: z.string() }), + })], +) satisfies ZodType; + +export const deprecatedDatasourceSchema = deprecatedBaseDatasourceSchema + .and(datasourceSpecSchema) satisfies ZodType; + +export const datasourceSchema = z.object({ + id: z.number(), + name: z.string(), +}).and(datasourceSpecSchema) satisfies ZodType; const uploadSchema = z.object({ id: z.number(), @@ -127,29 +150,16 @@ const datasourceOverviewSchema = z.object({ relationships: totalSchema.optional(), }) satisfies ZodType; -const vectorIndexErrorSchema = z.object({ - document_id: z.number(), - document_name: z.string(), - source_uri: z.string(), - error: z.string().nullable(), -}) satisfies ZodType; - -const kgIndexErrorSchema = z.object({ - chunk_id: z.string(), - source_uri: z.string(), - error: z.string().nullable(), -}) satisfies ZodType; - -export async function listDataSources ({ page = 1, size = 10 }: PageParams = {}): Promise> { +export async function listDataSources ({ page = 1, size = 10 }: PageParams = {}): Promise> { return fetch(requestUrl('/api/v1/admin/datasources', { page, size }), { headers: await authenticationHeaders(), - }).then(handleResponse(zodPage(datasourceSchema))); + }).then(handleResponse(zodPage(deprecatedDatasourceSchema))); } -export async function getDatasource (id: number): Promise { +export async function getDatasource (id: number): Promise { return fetch(requestUrl(`/api/v1/admin/datasources/${id}`), { headers: await authenticationHeaders(), - }).then(handleResponse(datasourceSchema)); + }).then(handleResponse(deprecatedDatasourceSchema)); } export async function deleteDatasource (id: number): Promise { @@ -159,12 +169,6 @@ export async function deleteDatasource (id: number): Promise { }).then(handleErrors); } -export async function getDatasourceOverview (id: number): Promise { - return fetch(requestUrl(`/api/v1/admin/datasources/${id}/overview`), { - headers: await authenticationHeaders(), - }).then(handleResponse(datasourceOverviewSchema)); -} - export async function createDatasource (params: CreateDatasourceParams) { return fetch(requestUrl(`/api/v1/admin/datasources`), { method: 'POST', @@ -173,7 +177,7 @@ export async function createDatasource (params: CreateDatasourceParams) { 'Content-Type': 'application/json', }, body: JSON.stringify(params), - }).then(handleResponse(datasourceSchema)); + }).then(handleResponse(deprecatedDatasourceSchema)); } export async function uploadFiles (files: File[]) { @@ -190,25 +194,3 @@ export async function uploadFiles (files: File[]) { body: formData, }).then(handleResponse(uploadSchema.array())); } - -export async function listDatasourceVectorIndexErrors (id: number, { page = 1, size = 10 }: PageParams = {}) { - return fetch(requestUrl(`/api/v1/admin/datasources/${id}/vector-index-errors`, { page, size }), { - headers: await authenticationHeaders(), - }).then(handleResponse(zodPage(vectorIndexErrorSchema))); -} - -export async function listDatasourceKgIndexErrors (id: number, { page = 1, size = 10 }: PageParams = {}) { - return fetch(requestUrl(`/api/v1/admin/datasources/${id}/kg-index-errors`, { page, size }), { - headers: await authenticationHeaders(), - }).then(handleResponse(zodPage(kgIndexErrorSchema))); -} - -export async function retryDatasourceAllFailedTasks (id: number) { - return fetch(requestUrl(`/api/v1/admin/datasources/${id}/retry-failed-tasks`), { - method: 'POST', - headers: { - ...await authenticationHeaders(), - 'Content-Type': 'application/json', - }, - }).then(handleErrors); -} \ No newline at end of file diff --git a/frontend/app/src/api/documents.ts b/frontend/app/src/api/documents.ts index 8a61a80f..e3cf4459 100644 --- a/frontend/app/src/api/documents.ts +++ b/frontend/app/src/api/documents.ts @@ -14,11 +14,31 @@ export const mimeTypes = [ const mimeValues: (typeof mimeTypes)[number]['value'] = mimeTypes.map(m => m.value) as never; +//"id": 396505, +// "hash": "1022309282298755521", +// "name": "b (1).txt", +// "content": "abc", +// "mime_type": "text/plain", +// "source_uri": "uploads/01907db88850795d855b552663c18c9f/1731058150-01930b1b2df979fd80b6f9dea8d0328e.txt", +// "meta": {}, +// "index_status": "completed", +// "index_result": null, +// "data_source": { +// "id": 630003, +// "name": "Test" +// }, +// "knowledge_base": { +// "id": 1, +// "name": "Lorem Ipsum" +// }, +// "last_modified_at": "2024-11-08T09:29:10" +// + export interface Document { id: number, name: string, - created_at: Date; - updated_at: Date + created_at?: Date | undefined; + updated_at?: Date | undefined last_modified_at: Date, hash: string content: string @@ -27,7 +47,14 @@ export interface Document { source_uri: string, index_status: string, index_result?: unknown - data_source_id: number + data_source: { + id: number + name: string + } + knowledge_base: { + id: number + name: string + } | null } const documentSchema = z.object({ @@ -43,7 +70,14 @@ const documentSchema = z.object({ source_uri: z.string(), index_status: z.string(), index_result: z.unknown(), - data_source_id: z.number(), + data_source: z.object({ + id: z.number(), + name: z.string(), + }), + knowledge_base: z.object({ + id: z.number(), + name: z.string(), + }).nullable(), }) satisfies ZodType; const zDate = z.coerce.date().or(z.literal('').transform(() => undefined)).optional(); @@ -51,7 +85,7 @@ const zDate = z.coerce.date().or(z.literal('').transform(() => undefined)).optio export const listDocumentsFiltersSchema = z.object({ name: z.string().optional(), source_uri: z.string().optional(), - data_source_id: z.coerce.number().optional(), + knowledge_base_id: z.coerce.number().optional(), created_at_start: zDate, created_at_end: zDate, updated_at_start: zDate, @@ -64,8 +98,8 @@ export const listDocumentsFiltersSchema = z.object({ export type ListDocumentsTableFilters = z.infer; -export async function listDocuments ({ page = 1, size = 10, ...filters }: PageParams & ListDocumentsTableFilters = {}): Promise> { - return await fetch(requestUrl('/api/v1/admin/documents', { page, size, ...filters }), { +export async function listDocuments ({ page = 1, size = 10, knowledge_base_id, ...filters }: PageParams & ListDocumentsTableFilters = {}): Promise> { + return await fetch(requestUrl(knowledge_base_id != null ? `/api/v1/admin/knowledge_bases/${knowledge_base_id}/documents` : '/api/v1/admin/documents', { page, size, ...filters }), { headers: await authenticationHeaders(), }) .then(handleResponse(zodPage(documentSchema))); diff --git a/frontend/app/src/api/embedding-model.ts b/frontend/app/src/api/embedding-models.ts similarity index 58% rename from frontend/app/src/api/embedding-model.ts rename to frontend/app/src/api/embedding-models.ts index fe575828..bfbfcc02 100644 --- a/frontend/app/src/api/embedding-model.ts +++ b/frontend/app/src/api/embedding-models.ts @@ -1,16 +1,21 @@ import { type ProviderOption, providerOptionSchema } from '@/api/providers'; -import { authenticationHeaders, handleNullableResponse, handleResponse, requestUrl } from '@/lib/request'; +import { authenticationHeaders, handleNullableResponse, handleResponse, type PageParams, requestUrl, zodPage } from '@/lib/request'; import { zodJsonDate } from '@/lib/zod'; import { z, type ZodType, type ZodTypeDef } from 'zod'; -export interface EmbeddingModel { +export interface EmbeddingModelSummary { id: number; name: string; provider: string; model: string; + vector_dimension: number, + is_default: boolean +} + +export interface EmbeddingModel extends EmbeddingModelSummary { config?: any; - created_at: Date | null; - updated_at: Date | null; + created_at?: Date | null; + updated_at?: Date | null; } export interface EmbeddingModelOption extends ProviderOption { @@ -26,14 +31,19 @@ export interface CreateEmbeddingModel { credentials: string | object; } -const embeddingModelSchema = z.object({ +export const embeddingModelSummarySchema = z.object({ id: z.number(), name: z.string(), provider: z.string(), model: z.string(), + vector_dimension: z.number(), + is_default: z.boolean(), +}) satisfies ZodType; + +const embeddingModelSchema = embeddingModelSummarySchema.extend({ config: z.any(), - created_at: zodJsonDate().nullable(), - updated_at: zodJsonDate().nullable(), + created_at: zodJsonDate().nullable().optional(), + updated_at: zodJsonDate().nullable().optional(), }) satisfies ZodType; const embeddingModelOptionSchema = providerOptionSchema.and(z.object({ @@ -42,21 +52,38 @@ const embeddingModelOptionSchema = providerOptionSchema.and(z.object({ })) satisfies ZodType; export async function listEmbeddingModelOptions () { - return await fetch(requestUrl(`/api/v1/admin/embedding-model/options`), { + return await fetch(requestUrl(`/api/v1/admin/embedding-models/options`), { headers: await authenticationHeaders(), }) .then(handleResponse(embeddingModelOptionSchema.array())); } +/** + * @deprecated + */ export async function getEmbeddingModel () { - return await fetch(requestUrl(`/api/v1/admin/embedding-model`), { + return await fetch(requestUrl(`/api/v1/admin/embedding-models`), { headers: await authenticationHeaders(), }) .then(handleNullableResponse(embeddingModelSchema)); } +export async function getEmbeddingModelById (id: number) { + return await fetch(requestUrl(`/api/v1/admin/embedding-models/${id}`), { + headers: await authenticationHeaders(), + }) + .then(handleResponse(embeddingModelSchema)); +} + +export async function listEmbeddingModels (params: PageParams) { + return await fetch(requestUrl(`/api/v1/admin/embedding-models`), { + headers: await authenticationHeaders(), + }) + .then(handleResponse(zodPage(embeddingModelSchema))); +} + export async function createEmbeddingModel (create: CreateEmbeddingModel) { - return await fetch(requestUrl(`/api/v1/admin/embedding-model`), { + return await fetch(requestUrl(`/api/v1/admin/embedding-models`), { method: 'POST', body: JSON.stringify(create), headers: { @@ -67,7 +94,7 @@ export async function createEmbeddingModel (create: CreateEmbeddingModel) { } export async function testEmbeddingModel (createEmbeddingModel: CreateEmbeddingModel) { - return await fetch(requestUrl(`/api/v1/admin/embedding-model/test`), { + return await fetch(requestUrl(`/api/v1/admin/embedding-models/test`), { method: 'POST', body: JSON.stringify(createEmbeddingModel), headers: { diff --git a/frontend/app/src/api/knowledge-base.ts b/frontend/app/src/api/knowledge-base.ts new file mode 100644 index 00000000..59232c4f --- /dev/null +++ b/frontend/app/src/api/knowledge-base.ts @@ -0,0 +1,238 @@ +/* + POST + /api/v1/admin/knowledge_bases + Create Knowledge Base + + + + GET + /api/v1/admin/knowledge_bases + List Knowledge Bases + + + + GET + /api/v1/admin/knowledge_bases/{knowledge_base_id} + Get Knowledge Base + PUT /api/v1/admin/knowledge_bases/{knowledge_base_id} + Update Knowledge Base Setting + DELETE /api/v1/admin/knowledge_bases/{knowledge_base_id} + Delete Knowledge Base + GET + /api/v1/admin/knowledge_bases/{knowledge_base_id}/overview + Get Knowledge Base Index Overview + GET + /api/v1/admin/knowledge_bases/{kb_id}/documents + List Knowledge Base Documents + GET + /api/v1/admin/knowledge_bases/{kb_id}/documents/{doc_id}/chunks + List Knowledge Base Chunks + POST + /api/v1/admin/knowledge_bases/{kb_id}/documents/reindex + Batch Reindex Knowledge Base Documents + POST + /api/v1/admin/knowledge_bases/{kb_id}/retry-failed-index-tasks + Retry Failed Tasks + */ + +import { type BaseCreateDatasourceParams, type CreateDatasourceSpecParams, type Datasource, type DatasourceKgIndexError, datasourceSchema, type DatasourceVectorIndexError } from '@/api/datasources'; +import { type EmbeddingModelSummary, embeddingModelSummarySchema } from '@/api/embedding-models'; +import { type LLMSummary, llmSummarySchema } from '@/api/llms'; +import { type IndexProgress, indexSchema, indexStatusSchema, type IndexTotalStats, totalSchema } from '@/api/rag'; +import { authenticationHeaders, handleErrors, handleResponse, type PageParams, requestUrl, zodPage } from '@/lib/request'; +import { zodJsonDate } from '@/lib/zod'; +import { z, type ZodType } from 'zod'; + +/** + * { + * "id": 1, + * "name": "Lorem Ipsum", + * "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + * "data_sources": [ + * { + * "id": 630003, + * "name": "Test", + * "data_source_type": "file", + * "config": [ + * { + * "file_id": 300004, + * "file_name": "b (1).txt" + * } + * ] + * } + * ], + * "index_methods": [ + * "vector" + * ], + * "llm": { + * "id": 30002, + * "name": "gemini-1.5-flash", + * "provider": "gemini", + * "model": "models/gemini-1.5-flash", + * "is_default": true + * }, + * "embedding_model": { + * "id": 60001, + * "name": "test", + * "model": "text-embedding-3-small", + * "vector_dimension": 0, + * "is_default": true + * }, + * "creator": { + * "id": "01907db8-8850-795d-855b-552663c18c9f" + * }, + * "created_at": "2024-11-08T09:29:11", + * "updated_at": "2024-11-08T09:29:11" + * } + */ + +export interface CreateKnowledgeBaseParams { + name: string; + description: string; + index_methods: ('vector' | 'knowledge_graph')[]; + llm_id?: number | null; + embedding_model_id?: number | null; + data_sources: (BaseCreateDatasourceParams & CreateDatasourceSpecParams)[]; +} + +export interface KnowledgeBaseSummary { + id: number; + name: string; + description: string; + index_methods: ('vector' | 'knowledge_graph')[]; + created_at: Date; + updated_at: Date; + creator: { + id: string; + }; +} + +export interface KnowledgeBase extends KnowledgeBaseSummary { + data_sources: Datasource[]; + llm?: LLMSummary | null; + embedding_model?: EmbeddingModelSummary | null; +} + +export type KnowledgeGraphIndexProgress = { + vector_index: IndexProgress + documents: IndexTotalStats + chunks: IndexTotalStats + kg_index?: IndexProgress + relationships?: IndexTotalStats +} + +export type KnowledgeGraphDocumentChunk = z.infer; + +const knowledgeBaseSummarySchema = z.object({ + id: z.number(), + name: z.string(), + description: z.string(), + index_methods: z.enum(['vector', 'knowledge_graph']).array(), + created_at: zodJsonDate(), + updated_at: zodJsonDate(), + creator: z.object({ + id: z.string(), + }), +}) satisfies ZodType; + +const knowledgeBaseSchema = knowledgeBaseSummarySchema.extend({ + data_sources: datasourceSchema.array(), + llm: llmSummarySchema.nullable().optional(), + embedding_model: embeddingModelSummarySchema.nullable().optional(), +}) satisfies ZodType; + +const knowledgeGraphIndexProgressSchema = z.object({ + vector_index: indexSchema, + documents: totalSchema, + chunks: totalSchema, + kg_index: indexSchema.optional(), + relationships: totalSchema.optional(), +}) satisfies ZodType; + +const knowledgeGraphDocumentChunkSchema = z.object({ + id: z.string(), + document_id: z.number(), + hash: z.string(), + text: z.string(), + meta: z.object({}).passthrough(), + embedding: z.number().array(), + relations: z.any(), + source_uri: z.string(), + index_status: indexStatusSchema, + index_result: z.string().nullable(), + created_at: zodJsonDate(), + updated_at: zodJsonDate(), +}); + +const vectorIndexErrorSchema = z.object({ + document_id: z.number(), + document_name: z.string(), + source_uri: z.string(), + error: z.string().nullable(), +}) satisfies ZodType; + +const kgIndexErrorSchema = z.object({ + chunk_id: z.string(), + source_uri: z.string(), + error: z.string().nullable(), +}) satisfies ZodType; + +export async function listKnowledgeBases ({ page = 1, size = 10 }: PageParams) { + return await fetch(requestUrl('/api/v1/admin/knowledge_bases', { page, size }), { + headers: await authenticationHeaders(), + }) + .then(handleResponse(zodPage(knowledgeBaseSummarySchema))); +} + +export async function getKnowledgeBaseById (id: number) { + return await fetch(requestUrl(`/api/v1/admin/knowledge_bases/${id}`), { + headers: await authenticationHeaders(), + }) + .then(handleResponse(knowledgeBaseSchema)); +} + +export async function getKnowledgeBaseDocumentChunks (id: number, documentId: number) { + return await fetch(requestUrl(`/api/v1/admin/knowledge_bases/${id}/documents/${documentId}/chunks`), { + headers: await authenticationHeaders(), + }) + .then(handleResponse(knowledgeGraphDocumentChunkSchema.array())); +} + +export async function createKnowledgeBase (params: CreateKnowledgeBaseParams) { + return await fetch(requestUrl('/api/v1/admin/knowledge_bases'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...await authenticationHeaders(), + }, + body: JSON.stringify(params), + }).then(handleResponse(knowledgeBaseSchema)); +} + +export async function getKnowledgeGraphIndexProgress (id: number): Promise { + return fetch(requestUrl(`/api/v1/admin/knowledge_bases/${id}/overview`), { + headers: await authenticationHeaders(), + }).then(handleResponse(knowledgeGraphIndexProgressSchema)); +} + +export async function listKnowledgeBaseVectorIndexErrors (id: number, { page = 1, size = 10 }: PageParams = {}) { + return fetch(requestUrl(`/api/v1/admin/knowledge_base/${id}/vector-index-errors`, { page, size }), { + headers: await authenticationHeaders(), + }).then(handleResponse(zodPage(vectorIndexErrorSchema))); +} + +export async function listKnowledgeBaseKgIndexErrors (id: number, { page = 1, size = 10 }: PageParams = {}) { + return fetch(requestUrl(`/api/v1/admin/knowledge_base/${id}/kg-index-errors`, { page, size }), { + headers: await authenticationHeaders(), + }).then(handleResponse(zodPage(kgIndexErrorSchema))); +} + +export async function retryKnowledgeBaseAllFailedTasks (id: number) { + return fetch(requestUrl(`/api/v1/admin/knowledge_base/${id}/retry-failed-tasks`), { + method: 'POST', + headers: { + ...await authenticationHeaders(), + 'Content-Type': 'application/json', + }, + }).then(handleErrors); +} diff --git a/frontend/app/src/api/llms.ts b/frontend/app/src/api/llms.ts index ebe2ce84..b45676ce 100644 --- a/frontend/app/src/api/llms.ts +++ b/frontend/app/src/api/llms.ts @@ -3,13 +3,16 @@ import { authenticationHeaders, handleErrors, handleResponse, type Page, type Pa import { zodJsonDate } from '@/lib/zod'; import { z, type ZodType, type ZodTypeDef } from 'zod'; -export interface LLM { +export interface LLMSummary { id: number; name: string; provider: string; model: string; - config?: any; is_default: boolean; +} + +export interface LLM extends LLMSummary { + config?: any; created_at: Date | null; updated_at: Date | null; } @@ -28,13 +31,16 @@ export interface CreateLLM { credentials: string | object; } -const llmSchema = z.object({ +export const llmSummarySchema = z.object({ id: z.number(), name: z.string(), provider: z.string(), model: z.string(), - config: z.any(), is_default: z.boolean(), +}) satisfies ZodType; + +const llmSchema = llmSummarySchema.extend({ + config: z.any(), created_at: zodJsonDate().nullable(), updated_at: zodJsonDate().nullable(), }) satisfies ZodType; diff --git a/frontend/app/src/api/rag.ts b/frontend/app/src/api/rag.ts index 0a8ff0e5..d551d8e4 100644 --- a/frontend/app/src/api/rag.ts +++ b/frontend/app/src/api/rag.ts @@ -1,4 +1,3 @@ -import { authenticationHeaders, handleResponse, requestUrl } from '@/lib/request'; import { z, type ZodType } from 'zod'; export const indexStatuses = [ @@ -17,14 +16,7 @@ export type IndexTotalStats = { total: number } -export interface RagIndexProgress { - kg_index: IndexProgress; - vector_index: IndexProgress; - documents: IndexTotalStats; - chunks: IndexTotalStats; - entities: IndexTotalStats; - relationships: IndexTotalStats; -} +export const indexStatusSchema = z.enum(indexStatuses) satisfies ZodType; export const totalSchema = z.object({ total: z.number(), @@ -37,19 +29,3 @@ export const indexSchema = z.object({ completed: z.number().optional(), failed: z.number().optional(), }) satisfies ZodType; - -const ragIndexProgressSchema = z.object({ - kg_index: indexSchema, - vector_index: indexSchema, - documents: totalSchema, - chunks: totalSchema, - entities: totalSchema, - relationships: totalSchema, -}) satisfies ZodType; - -export async function getIndexProgress () { - return await fetch(requestUrl('/api/v1/admin/rag/index-progress'), { - headers: await authenticationHeaders(), - }) - .then(handleResponse(ragIndexProgressSchema)); -} 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 index 23bb76dd..685e69e7 100644 --- a/frontend/app/src/app/(main)/(admin)/datasources/[id]/documents/page.tsx +++ b/frontend/app/src/app/(main)/(admin)/datasources/[id]/documents/page.tsx @@ -1,5 +1,6 @@ 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'; @@ -17,7 +18,8 @@ export default async function ChatEnginesPage ({ params }: { params: { id: strin { title: 'Documents' }, ]} /> - + + ); } diff --git a/frontend/app/src/app/(main)/(admin)/datasources/[id]/page.tsx b/frontend/app/src/app/(main)/(admin)/datasources/[id]/page.tsx index 6ccafc3d..1714c4aa 100644 --- a/frontend/app/src/app/(main)/(admin)/datasources/[id]/page.tsx +++ b/frontend/app/src/app/(main)/(admin)/datasources/[id]/page.tsx @@ -1,4 +1,5 @@ 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'; @@ -13,6 +14,7 @@ export default function DatasourcePage ({ params }: { params: { id: string } }) { title: , url: `/datasources/${id}` }, ]} /> + ); diff --git a/frontend/app/src/app/(main)/(admin)/datasources/create/[type]/page.tsx b/frontend/app/src/app/(main)/(admin)/datasources/create/[type]/page.tsx deleted file mode 100644 index 8365ffa9..00000000 --- a/frontend/app/src/app/(main)/(admin)/datasources/create/[type]/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -'use client'; - -import { CreateDatasourceForm } from '@/components/datasource/CreateDatasourceForm'; -import { isDatasourceType } from '@/components/datasource/types'; -import { notFound, useRouter } from 'next/navigation'; -import { useTransition } from 'react'; - -export default function CreateDatasourcePage ({ params }: { params: { type: string } }) { - const [transitioning, startTransition] = useTransition(); - const router = useRouter(); - - if (!isDatasourceType(params.type)) { - notFound(); - } - - return ( - { - startTransition(() => { - router.push(`/datasources/${datasource.id}`); - }); - }} - /> - ); -} diff --git a/frontend/app/src/app/(main)/(admin)/datasources/create/layout.tsx b/frontend/app/src/app/(main)/(admin)/datasources/create/layout.tsx deleted file mode 100644 index c83f5cee..00000000 --- a/frontend/app/src/app/(main)/(admin)/datasources/create/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import { AdminPageHeading } from '@/components/admin-page-heading'; -import { DatasourceTypeTabs } from '@/components/datasource/DatasourceTypeTabs'; -import { isDatasourceType } from '@/components/datasource/types'; -import { capitalCase } from 'change-case-all'; -import { notFound, useRouter, useSelectedLayoutSegment } from 'next/navigation'; -import type { ReactNode } from 'react'; - -export default function Layout ({ children }: { children: ReactNode }) { - const type = useSelectedLayoutSegment()!; - if (!isDatasourceType(type)) { - notFound(); - } - const router = useRouter(); - - return ( - <> - - router.push(`/datasources/create/${type}`)} /> -
- {children} -
- - ); -} \ 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 index 71cbfc2f..2b508362 100644 --- a/frontend/app/src/app/(main)/(admin)/datasources/page.tsx +++ b/frontend/app/src/app/(main)/(admin)/datasources/page.tsx @@ -1,10 +1,12 @@ 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)/embedding-model/page.tsx b/frontend/app/src/app/(main)/(admin)/embedding-model/page.tsx deleted file mode 100644 index 5b92a15c..00000000 --- a/frontend/app/src/app/(main)/(admin)/embedding-model/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client'; - -import { getEmbeddingModel } from '@/api/embedding-model'; -import { AdminPageHeading } from '@/components/admin-page-heading'; -import { ConfigViewer } from '@/components/config-viewer'; -import { DateFormat } from '@/components/date-format'; -import { OptionDetail } from '@/components/option-detail'; -import { Loader2Icon } from 'lucide-react'; -import useSWR from 'swr'; - -export default function EmbeddingModelPage () { - const { data: embeddingModel, isLoading } = useSWR('api.embedding-models.get', () => getEmbeddingModel()); - - if (isLoading) { - return <> - - - ; - } - - if (!embeddingModel) { - return ( - <> - -
- Embedding Model not configured. -
- - ); - } - - return ( - <> - -
-
- - - - - } /> - } /> - } /> -
-
- - ); -} diff --git a/frontend/app/src/app/(main)/(admin)/embedding-models/create/page.tsx b/frontend/app/src/app/(main)/(admin)/embedding-models/create/page.tsx new file mode 100644 index 00000000..c13d22dd --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/embedding-models/create/page.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { AdminPageHeading } from '@/components/admin-page-heading'; +import { CreateEmbeddingModelForm } from '@/components/embedding-models/CreateEmbeddingModelForm'; +import { useRouter } from 'next/navigation'; +import { useTransition } from 'react'; + +export default function Page () { + const router = useRouter(); + const [transitioning, startTransition] = useTransition(); + + return ( + <> + + { + startTransition(() => { + router.push(`/embedding-models/${embeddingModel.id}`); + }); + }} + /> + + ); +} \ No newline at end of file diff --git a/frontend/app/src/app/(main)/(admin)/embedding-models/page.tsx b/frontend/app/src/app/(main)/(admin)/embedding-models/page.tsx new file mode 100644 index 00000000..e15e2172 --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/embedding-models/page.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { AdminPageHeading } from '@/components/admin-page-heading'; +import { ConfigViewer } from '@/components/config-viewer'; +import { DateFormat } from '@/components/date-format'; +import { EmbeddingModelsTable } from '@/components/embedding-models/EmbeddingModelsTable'; +import { OptionDetail } from '@/components/option-detail'; + +export default function EmbeddingModelPage () { + + return ( + <> + + + {/*
*/} + {/*
*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* } />*/} + {/* } />*/} + {/* } />*/} + {/*
*/} + {/*
*/} + + ); +} diff --git a/frontend/app/src/app/(main)/(admin)/index-progress/page.tsx b/frontend/app/src/app/(main)/(admin)/index-progress/page.tsx deleted file mode 100644 index 5909b2fb..00000000 --- a/frontend/app/src/app/(main)/(admin)/index-progress/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import { getIndexProgress } from '@/api/rag'; -import { AdminPageHeading } from '@/components/admin-page-heading'; -import { IndexProgressChart, IndexProgressChartPlaceholder } from '@/components/charts/IndexProgressChart'; -import { TotalCard } from '@/components/charts/TotalCard'; -import { ArrowRightIcon, FileTextIcon, MapPinIcon, PuzzleIcon, RouteIcon } from 'lucide-react'; -import Link from 'next/link'; -import useSWR from 'swr'; - -export default function IndexProgressPage () { - const { data: progress } = useSWR('api.rag.index-progress', () => getIndexProgress()); - - return ( - <> - -
- } - total={progress?.documents.total} - > - All documents - - } total={progress?.chunks.total} /> - } - total={progress?.entities.total} - > - Graph Editor - - } total={progress?.relationships.total} /> -
-
- {progress ? : } - {progress ? : } -
- - ); -} diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/api.ts b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/api.ts new file mode 100644 index 00000000..32eac84b --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/api.ts @@ -0,0 +1,4 @@ +import { getKnowledgeBaseById } from '@/api/knowledge-base'; +import { cache } from 'react'; + +export const cachedGetKnowledgeBaseById = cache(getKnowledgeBaseById); \ 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 new file mode 100644 index 00000000..f8a331e6 --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/context.tsx @@ -0,0 +1,18 @@ +'use client'; + +import type { KnowledgeBase } from '@/api/knowledge-base'; +import { createContext, type ReactNode, useContext } from 'react'; + +const KBContext = createContext(null as any); + +export function KBProvider ({ children, value }: { children: ReactNode, value: KnowledgeBase }) { + return ( + + {children} + + ); +} + +export function useKB () { + return useContext(KBContext); +} 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 new file mode 100644 index 00000000..c2de24db --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/index-progress/page.tsx @@ -0,0 +1,17 @@ +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]/layout.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/layout.tsx new file mode 100644 index 00000000..827d6c65 --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/layout.tsx @@ -0,0 +1,22 @@ +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]/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/page.tsx new file mode 100644 index 00000000..7654019e --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/page.tsx @@ -0,0 +1,11 @@ +import { DocumentsTable } from '@/components/documents/documents-table'; + +export default async function KnowledgeBasePage ({ 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]/settings/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/settings/page.tsx new file mode 100644 index 00000000..12816f4d --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/settings/page.tsx @@ -0,0 +1,46 @@ +import { cachedGetKnowledgeBaseById } from '@/app/(main)/(admin)/knowledge-bases/[id]/api'; +import { KBProvider } from '@/app/(main)/(admin)/knowledge-bases/[id]/context'; +import { DateFormat } from '@/components/date-format'; +import { KnowledgeBaseDatasourceDetails } from '@/components/knowledge-base/datasource-details'; +import { ModelComponentInfo } from '@/components/model-component-info'; +import { OptionDetail } from '@/components/option-detail'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; + +export default async function KnowledgeBaseSettingsPage ({ params }: { params: { id: string } }) { + const id = parseInt(decodeURIComponent(params.id)); + const kb = await cachedGetKnowledgeBaseById(id); + + return ( + +
+

Details

+
+
+ + + + `/llms/${llm.id}`} />} /> + `/embedding-models/${em.id}`} />} /> + } /> + } /> +
+
+
+
+

Datasources

+ + {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]/tabs.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/tabs.tsx new file mode 100644 index 00000000..86eb3271 --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/[id]/tabs.tsx @@ -0,0 +1,47 @@ +'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}/index-progress`); + })} + > + Index Progress + + 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/new/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/new/page.tsx new file mode 100644 index 00000000..979e1457 --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/new/page.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { AdminPageHeading } from '@/components/admin-page-heading'; +import { CreateKnowledgeBaseForm } from '@/components/knowledge-base/create-knowledge-base-form'; + +export default function NewKnowledgeBasePage () { + return ( + <> + + + + ); +} diff --git a/frontend/app/src/app/(main)/(admin)/knowledge-bases/page.tsx b/frontend/app/src/app/(main)/(admin)/knowledge-bases/page.tsx new file mode 100644 index 00000000..e16a334b --- /dev/null +++ b/frontend/app/src/app/(main)/(admin)/knowledge-bases/page.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { listKnowledgeBases } from '@/api/knowledge-base'; +import { AdminPageHeading } from '@/components/admin-page-heading'; +import { KnowledgeBaseCard } from '@/components/knowledge-base/knowledge-base-card'; +import { NextLink } from '@/components/nextjs/NextLink'; +import type { PaginationState } from '@tanstack/table-core'; +import { useState } from 'react'; +import useSWR from 'swr'; + +export default function KnowledgeBasesPage () { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + + const { data, mutate, isLoading, isValidating } = useSWR(`api.knowledge-bases.list?page=${pagination.pageIndex}&size=${pagination.pageSize}`, () => listKnowledgeBases({ page: pagination.pageIndex + 1, size: pagination.pageSize }), { + revalidateOnReconnect: false, + revalidateOnFocus: false, + focusThrottleInterval: 1000, + keepPreviousData: true, + onError: console.error, + }); + + return ( + <> + + + New Knowledge Base + +
+ {data?.items.map(kb => ( + + + Details + + + ))} +
+ + ); +} + +export const dynamic = 'force-dynamic'; diff --git a/frontend/app/src/app/(main)/nav.tsx b/frontend/app/src/app/(main)/nav.tsx index 7a67b6a0..713c2388 100644 --- a/frontend/app/src/app/(main)/nav.tsx +++ b/frontend/app/src/app/(main)/nav.tsx @@ -5,14 +5,18 @@ import { ChatNewDialog } from '@/components/chat/chat-new-dialog'; import { ChatsHistory } from '@/components/chat/chats-history'; import { type NavGroup, SiteNav } from '@/components/site-nav'; import { SiteNavFooter } from '@/components/site-nav-footer'; +import { useBootstrapStatus } from '@/components/system/BootstrapStatusProvider'; import { Button } from '@/components/ui/button'; import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'; import { ScrollArea } from '@/components/ui/scroll-area'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { useHref } from '@/components/use-href'; -import { ActivitySquareIcon, BinaryIcon, BotMessageSquareIcon, BrainCircuitIcon, CogIcon, FilesIcon, GaugeIcon, HomeIcon, KeyRoundIcon, LibraryIcon, MenuIcon, MessageCircleQuestionIcon, MessagesSquareIcon, ShuffleIcon, WaypointsIcon } from 'lucide-react'; +import { ActivitySquareIcon, AlertTriangleIcon, BinaryIcon, BotMessageSquareIcon, BrainCircuitIcon, CogIcon, FilesIcon, HomeIcon, KeyRoundIcon, LibraryBigIcon, LibraryIcon, MenuIcon, MessageCircleQuestionIcon, MessagesSquareIcon, ShuffleIcon } from 'lucide-react'; import Link from 'next/link'; +import type { ReactNode } from 'react'; export function Nav () { + const { required } = useBootstrapStatus(); const href = useHref(); const auth = useAuth(); const user = auth.me; @@ -39,13 +43,14 @@ export function Nav () { { href: '/stats/trending', title: 'Stats', icon: ActivitySquareIcon }, { href: '/feedbacks', title: 'Feedbacks', icon: MessageCircleQuestionIcon }, { href: '/documents', title: 'Documents', icon: FilesIcon }, - { href: '/datasources', title: 'Datasources', icon: LibraryIcon }, + { href: '/knowledge-bases', title: 'Knowledge Bases', icon: LibraryBigIcon }, + { href: '/datasources', title: 'Datasources', icon: LibraryIcon, details: !required.datasource && You need to configure at least one Datasource. }, { href: '/chat-engines', title: 'Chat Engines', icon: BotMessageSquareIcon }, - { href: '/llms', title: 'LLMs', icon: BrainCircuitIcon }, - { href: '/embedding-model', title: 'Embedding Model', icon: BinaryIcon }, + { href: '/llms', title: 'LLMs', icon: BrainCircuitIcon, details: !required.default_llm && You need to configure at least one Default LLM. }, + { href: '/embedding-models', title: 'Embedding Models', icon: BinaryIcon, details: !required.default_embedding_model && You need to configure at least one Default Embedding Model. }, { href: '/reranker-models', title: 'Reranker Models', icon: ShuffleIcon }, - { href: '/index-progress', title: 'Index Progress', icon: GaugeIcon }, - { href: '/knowledge-graph', title: 'Knowledge Graph', icon: WaypointsIcon }, + // { href: '/index-progress', title: 'Index Progress', icon: GaugeIcon }, + // { href: '/knowledge-graph', title: 'Knowledge Graph', icon: WaypointsIcon }, { href: '/site-settings', title: 'Settings', icon: CogIcon }, ], sectionProps: { className: 'mt-auto mb-0' }, @@ -85,3 +90,18 @@ export function NavDrawer () { ); } + +function NavWarningDetails ({ children }: { children: ReactNode }) { + return ( + + + + + + + {children} + + + + ); +} diff --git a/frontend/app/src/app/(main)/page.tsx b/frontend/app/src/app/(main)/page.tsx index e20ce474..02736f2a 100644 --- a/frontend/app/src/app/(main)/page.tsx +++ b/frontend/app/src/app/(main)/page.tsx @@ -3,6 +3,7 @@ import { Ask } from '@/components/chat/ask'; import { useAsk } from '@/components/chat/use-ask'; import { withReCaptcha } from '@/components/security-setting-provider'; +import { SystemWizardBanner } from '@/components/system/SystemWizardBanner'; import { Button } from '@/components/ui/button'; import { useSettingContext } from '@/components/website-setting-provider'; import NextLink from 'next/link'; @@ -14,7 +15,8 @@ export default function Page () { const { homepage_title, description, homepage_example_questions, homepage_footer_links } = useSettingContext(); return ( -
+
+

{homepage_title || ''} diff --git a/frontend/app/src/app/layout.tsx b/frontend/app/src/app/layout.tsx index 83ae6b1d..9912b49a 100644 --- a/frontend/app/src/app/layout.tsx +++ b/frontend/app/src/app/layout.tsx @@ -1,17 +1,17 @@ import { getPublicSiteSettings } from '@/api/site-settings'; import { getBootstrapStatus } from '@/api/system'; import { RootProviders } from '@/app/RootProviders'; -import { SystemWizardDialog } from '@/components/system/SystemWizardDialog'; import { experimentalFeatures } from '@/experimental/experimental-features'; import { auth } from '@/lib/auth'; import { GoogleAnalytics } from '@next/third-parties/google'; import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; -import './globals.css'; -import './chart-theme.css'; import Script from 'next/script'; import { cache, type ReactNode } from 'react'; +import './globals.css'; +import './chart-theme.css'; + const inter = Inter({ subsets: ['latin'] }); const cachedGetSettings = cache(getPublicSiteSettings); @@ -51,7 +51,6 @@ export default async function RootLayout ({ {children} - {settings.ga_id && }