From 714569edac3304a429c8f79bf8b257db4a84d718 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Thu, 6 Mar 2025 11:25:38 -0500 Subject: [PATCH 1/7] Update knowledge and skill wizard info and doc upload pages Signed-off-by: Jeffrey Phillips --- src/app/api/github/git-info/route.ts | 105 ++++++ src/app/api/github/knowledge-files/route.ts | 119 +------ src/app/api/github/utils.ts | 114 +++++++ src/app/api/native/convert-http/route.ts | 11 +- src/app/api/native/git/info/route.ts | 105 ++++++ .../api/native/git/knowledge-files/route.ts | 75 +--- src/app/api/native/git/utils.ts | 78 +++++ src/app/contribute/knowledge/page.tsx | 29 +- .../Common/WizardFormGroupLabelHelp.tsx | 20 ++ .../WizardPageHeader.tsx} | 6 +- src/components/Common/WizardSectionHeader.tsx | 32 ++ .../AttributionInformation.tsx | 80 +++-- .../AuthorInformation/AuthorInformation.tsx | 94 ----- src/components/Contribute/AutoFill.ts | 3 +- .../ContributionWizard/ContributionWizard.tsx | 25 +- .../Contribute/DetailsPage/DetailsPage.tsx | 322 ++++++++++++++++++ .../EditKnowledge/github/EditKnowledge.tsx | 3 +- .../EditKnowledge/native/EditKnowledge.tsx | 3 +- .../FilePathInformation.tsx | 32 -- .../DocumentInformation.tsx | 280 +++------------ .../KnowledgeInformation.tsx | 125 ------- .../KnowledgeFileSelectModal.tsx | 4 - .../KnowledgeSeedExamples.tsx | 4 +- .../KnowledgeWizard/KnowledgeWizard.tsx | 251 +++++++++----- .../Knowledge/MultFileUploadArea.tsx | 50 +++ .../Contribute/Knowledge/UploadFile.tsx | 53 +-- .../Knowledge/UploadFromGitModal.tsx | 219 ++++++++++++ .../ReviewSubmission/ReviewSubmission.tsx | 48 ++- .../SkillSeedExamples/SkillSeedExamples.tsx | 4 +- .../Skill/SkillWizard/SkillWizard.tsx | 49 +-- .../SkillsInformation/SkillsInformation.tsx | 91 ----- .../Contribute/Utils/submitUtils.ts | 16 +- .../Contribute/Utils/validationUtils.ts | 34 +- src/components/PathService/PathService.tsx | 24 +- src/types/index.ts | 1 + 35 files changed, 1513 insertions(+), 996 deletions(-) create mode 100644 src/app/api/github/git-info/route.ts create mode 100644 src/app/api/github/utils.ts create mode 100644 src/app/api/native/git/info/route.ts create mode 100644 src/app/api/native/git/utils.ts create mode 100644 src/components/Common/WizardFormGroupLabelHelp.tsx rename src/components/{Contribute/PageHeader.tsx => Common/WizardPageHeader.tsx} (64%) create mode 100644 src/components/Common/WizardSectionHeader.tsx delete mode 100644 src/components/Contribute/AuthorInformation/AuthorInformation.tsx create mode 100644 src/components/Contribute/DetailsPage/DetailsPage.tsx delete mode 100644 src/components/Contribute/FilePathInformation/FilePathInformation.tsx delete mode 100644 src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx create mode 100644 src/components/Contribute/Knowledge/MultFileUploadArea.tsx create mode 100644 src/components/Contribute/Knowledge/UploadFromGitModal.tsx delete mode 100644 src/components/Contribute/Skill/SkillsInformation/SkillsInformation.tsx diff --git a/src/app/api/github/git-info/route.ts b/src/app/api/github/git-info/route.ts new file mode 100644 index 00000000..0c36a9c6 --- /dev/null +++ b/src/app/api/github/git-info/route.ts @@ -0,0 +1,105 @@ +// src/app/api/github/knowledge-files/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { getToken } from 'next-auth/jwt'; +import { + BASE_BRANCH, + checkIfRepoExists, + fetchCommitInfo, + fetchMarkdownFiles, + forkRepo, + getBranchSha, + GITHUB_API_URL, + TAXONOMY_DOCUMENTS_REPO +} from '@/app/api/github/utils'; + +export async function GET(req: NextRequest) { + const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET! }); + + if (!token || !token.accessToken) { + console.error('Unauthorized: Missing or invalid access token'); + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const githubToken = token.accessToken as string; + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${githubToken}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + }; + + try { + // Fetch GitHub username + const githubUsername = await getGitHubUsername(headers); + + // Split the TAXONOMY_DOCUMENTS_REPO into owner and repo name + const repoPath = TAXONOMY_DOCUMENTS_REPO.replace('github.com/', ''); + const [repoOwner, repoName] = repoPath.split('/'); + + // Check if the repository is already forked + const repoForked = await checkIfRepoExists(headers, githubUsername, repoName); + console.log(`Repository forked: ${repoForked}`); + if (!repoForked) { + // Fork the repository if it is not already forked + await forkRepo(headers, repoOwner, repoName, githubUsername); + // Add a delay to ensure the fork operation completes to avoid a race condition when retrieving the bas SHA + // This only occurs if this is the first time submitting and the fork isn't present. + // TODO change to a retry + console.log('Pause 5s for the forking operation to complete'); + await new Promise((resolve) => setTimeout(resolve, 5000)); + console.log('Repository forked'); + } + + // Fetch the latest commit SHA of the base branch + const baseBranchSha = await getBranchSha(headers, githubUsername, repoName, BASE_BRANCH); + console.log(`Base branch SHA: ${baseBranchSha}`); + + const files = await fetchMarkdownFiles(headers, githubUsername, repoName, BASE_BRANCH); + + let mostRecentSha = ''; + let mostRecentDate = 0; + let mostRecentFiles: string[] = []; + + for (const file of files) { + const commitInfo = await fetchCommitInfo(headers, githubUsername, repoName, file.path); + if (commitInfo) { + const { sha, date } = commitInfo; + const commitDate = new Date(date).getTime(); + if (commitDate > mostRecentDate) { + mostRecentDate = commitDate; + mostRecentSha = sha; + mostRecentFiles = []; + } + if (sha === mostRecentSha) { + mostRecentFiles.push(file.path); + } + } + } + const fileNames = mostRecentFiles.join(','); + + return NextResponse.json( + { + repoUrl: `https://github.com/${githubUsername}/${repoName}`, + commitSha: baseBranchSha, + fileNames + }, + { status: 201 } + ); + } catch (error) { + console.error('Failed to retrieve document info:', error); + return NextResponse.json({ error: 'Failed to retrieve document info' }, { status: 500 }); + } +} + +async function getGitHubUsername(headers: HeadersInit): Promise { + const response = await fetch(`${GITHUB_API_URL}/user`, { headers }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Failed to fetch GitHub username:', response.status, errorText); + throw new Error('Failed to fetch GitHub username'); + } + + const data = await response.json(); + return data.login; +} diff --git a/src/app/api/github/knowledge-files/route.ts b/src/app/api/github/knowledge-files/route.ts index 136a23f6..b5a268f4 100644 --- a/src/app/api/github/knowledge-files/route.ts +++ b/src/app/api/github/knowledge-files/route.ts @@ -1,10 +1,16 @@ // src/app/api/github/knowledge-files/route.ts import { NextRequest, NextResponse } from 'next/server'; import { getToken } from 'next-auth/jwt'; - -const GITHUB_API_URL = 'https://api.github.com'; -const TAXONOMY_DOCUMENTS_REPO = process.env.NEXT_PUBLIC_TAXONOMY_DOCUMENTS_REPO || 'https://github.com/instructlab-public/taxonomy-knowledge-docs'; -const BASE_BRANCH = 'main'; +import { + BASE_BRANCH, + checkIfRepoExists, + fetchCommitInfo, + fetchMarkdownFiles, + forkRepo, + getBranchSha, + GITHUB_API_URL, + TAXONOMY_DOCUMENTS_REPO +} from '@/app/api/github/utils'; // Interface for the response interface KnowledgeFile { @@ -100,62 +106,6 @@ async function getGitHubUsernameAndEmail(headers: HeadersInit): Promise<{ github return { githubUsername: data.login, userEmail: data.email }; } -async function checkIfRepoExists(headers: HeadersInit, owner: string, repo: string): Promise { - const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}`, { headers }); - const exists = response.ok; - if (!exists) { - const errorText = await response.text(); - console.error('Repository does not exist:', response.status, errorText); - } - return exists; -} - -async function forkRepo(headers: HeadersInit, owner: string, repo: string, forkOwner: string) { - const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/forks`, { - method: 'POST', - headers - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error('Failed to fork repository:', response.status, errorText); - throw new Error('Failed to fork repository'); - } - - // Wait for the fork to be created - let forkCreated = false; - for (let i = 0; i < 10; i++) { - const forkExists = await checkIfRepoExists(headers, forkOwner, repo); - if (forkExists) { - forkCreated = true; - break; - } - await new Promise((resolve) => setTimeout(resolve, 3000)); - } - - if (!forkCreated) { - throw new Error('Failed to confirm fork creation'); - } -} - -async function getBranchSha(headers: HeadersInit, owner: string, repo: string, branch: string): Promise { - console.log(`Fetching branch SHA for ${branch}...`); - const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/ref/heads/${branch}`, { headers }); - - if (!response.ok) { - const errorText = await response.text(); - console.error('Failed to get branch SHA:', response.status, errorText); - if (response.status === 409 && errorText.includes('Git Repository is empty')) { - throw new Error('Git Repository is empty.'); - } - throw new Error('Failed to get branch SHA'); - } - - const data = await response.json(); - console.log('Branch SHA:', data.object.sha); - return data.object.sha; -} - async function createFilesCommit( headers: HeadersInit, owner: string, @@ -292,55 +242,6 @@ export async function GET(req: NextRequest) { } } -// Fetch all markdown files from the main branch -async function fetchMarkdownFiles( - headers: HeadersInit, - owner: string, - repo: string, - branchName: string -): Promise<{ path: string; content: string }[]> { - try { - const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees/${branchName}?recursive=1`, { headers }); - if (!response.ok) { - const errorText = await response.text(); - console.error('Failed to fetch files from knowledge document repository:', response.status, errorText); - throw new Error('Failed to fetch file from knowledge document repository:'); - } - - const data = await response.json(); - const files = data.tree.filter( - (item: { type: string; path: string }) => item.type === 'blob' && item.path.endsWith('.md') && item.path !== 'README.md' - ); - return files.map((file: { path: string; content: string }) => ({ path: file.path, content: file.content })); - } catch (error) { - console.error('Error fetching files from knowledge document repository:', error); - return []; - } -} - -// Fetch the latest commit info for a file -async function fetchCommitInfo(headers: HeadersInit, owner: string, repo: string, filePath: string): Promise<{ sha: string; date: string } | null> { - try { - const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/commits?path=${filePath}`, { headers }); - if (!response.ok) { - const errorText = await response.text(); - console.error('Failed to fetch commit information for file:', response.status, errorText); - throw new Error('Failed to fetch commit information for file.'); - } - - const data = await response.json(); - if (data.length === 0) return null; - - return { - sha: data[0].sha, - date: data[0].commit.committer.date - }; - } catch (error) { - console.error(`Error fetching commit info for ${filePath}:`, error); - return null; - } -} - // Fetch the content of a file from the repository async function fetchFileContent(headers: HeadersInit, owner: string, repo: string, filePath: string): Promise { try { diff --git a/src/app/api/github/utils.ts b/src/app/api/github/utils.ts new file mode 100644 index 00000000..dbbfadca --- /dev/null +++ b/src/app/api/github/utils.ts @@ -0,0 +1,114 @@ +export const GITHUB_API_URL = 'https://api.github.com'; +export const TAXONOMY_DOCUMENTS_REPO = + process.env.NEXT_PUBLIC_TAXONOMY_DOCUMENTS_REPO || 'https://github.com/instructlab-public/taxonomy-knowledge-docs'; +export const BASE_BRANCH = 'main'; + +export const checkIfRepoExists = async (headers: HeadersInit, owner: string, repo: string): Promise => { + const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}`, { headers }); + const exists = response.ok; + if (!exists) { + const errorText = await response.text(); + console.error('Repository does not exist:', response.status, errorText); + } + return exists; +}; + +export const forkRepo = async (headers: HeadersInit, owner: string, repo: string, forkOwner: string) => { + const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/forks`, { + method: 'POST', + headers + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Failed to fork repository:', response.status, errorText); + throw new Error('Failed to fork repository'); + } + + // Wait for the fork to be created + let forkCreated = false; + for (let i = 0; i < 10; i++) { + const forkExists = await checkIfRepoExists(headers, forkOwner, repo); + if (forkExists) { + forkCreated = true; + break; + } + await new Promise((resolve) => setTimeout(resolve, 3000)); + } + + if (!forkCreated) { + throw new Error('Failed to confirm fork creation'); + } +}; + +export const getBranchSha = async (headers: HeadersInit, owner: string, repo: string, branch: string): Promise => { + console.log(`Fetching branch SHA for ${branch}...`); + const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/ref/heads/${branch}`, { headers }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Failed to get branch SHA:', response.status, errorText); + if (response.status === 409 && errorText.includes('Git Repository is empty')) { + throw new Error('Git Repository is empty.'); + } + throw new Error('Failed to get branch SHA'); + } + + const data = await response.json(); + console.log('Branch SHA:', data.object.sha); + return data.object.sha; +}; + +// Fetch all markdown files from the main branch +export const fetchMarkdownFiles = async ( + headers: HeadersInit, + owner: string, + repo: string, + branchName: string +): Promise<{ path: string; content: string }[]> => { + try { + const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees/${branchName}?recursive=1`, { headers }); + if (!response.ok) { + const errorText = await response.text(); + console.error('Failed to fetch files from knowledge document repository:', response.status, errorText); + throw new Error('Failed to fetch file from knowledge document repository:'); + } + + const data = await response.json(); + const files = data.tree.filter( + (item: { type: string; path: string }) => item.type === 'blob' && item.path.endsWith('.md') && item.path !== 'README.md' + ); + return files.map((file: { path: string; content: string }) => ({ path: file.path, content: file.content })); + } catch (error) { + console.error('Error fetching files from knowledge document repository:', error); + return []; + } +}; + +// Fetch the latest commit info for a file +export const fetchCommitInfo = async ( + headers: HeadersInit, + owner: string, + repo: string, + filePath: string +): Promise<{ sha: string; date: string } | null> => { + try { + const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${repo}/commits?path=${filePath}`, { headers }); + if (!response.ok) { + const errorText = await response.text(); + console.error('Failed to fetch commit information for file:', response.status, errorText); + throw new Error('Failed to fetch commit information for file.'); + } + + const data = await response.json(); + if (data.length === 0) return null; + + return { + sha: data[0].sha, + date: data[0].commit.committer.date + }; + } catch (error) { + console.error(`Error fetching commit info for ${filePath}:`, error); + return null; + } +}; diff --git a/src/app/api/native/convert-http/route.ts b/src/app/api/native/convert-http/route.ts index 804ff87d..2d7a1c7e 100644 --- a/src/app/api/native/convert-http/route.ts +++ b/src/app/api/native/convert-http/route.ts @@ -19,9 +19,9 @@ interface ConvertHttpRequestBody { // convert a doc from a URL (provided via http_sources) to Markdown. export async function POST(request: Request) { + const baseUrl = process.env.IL_FILE_CONVERSION_SERVICE || 'http://doclingserve:5001'; + try { - const body: ConvertHttpRequestBody = await request.json(); - const baseUrl = process.env.IL_FILE_CONVERSION_SERVICE || 'http://doclingserve:5001'; const healthRes = await fetch(`${baseUrl}/health`); if (!healthRes.ok) { console.error('The file conversion service is offline or returned non-OK status:', healthRes.status, healthRes.statusText); @@ -33,6 +33,13 @@ export async function POST(request: Request) { console.error('Conversion service health check response not "ok":', healthData); return NextResponse.json({ error: 'Conversion service is offline, only markdown files accepted.' }, { status: 503 }); } + } catch (error: unknown) { + console.error('Error conversion service health check:', error); + return NextResponse.json({ error: 'Conversion service is offline, only markdown files accepted.' }, { status: 503 }); + } + + try { + const body: ConvertHttpRequestBody = await request.json(); const res = await fetch(`${baseUrl}/v1alpha/convert/source`, { method: 'POST', diff --git a/src/app/api/native/git/info/route.ts b/src/app/api/native/git/info/route.ts new file mode 100644 index 00000000..d4d1599a --- /dev/null +++ b/src/app/api/native/git/info/route.ts @@ -0,0 +1,105 @@ +// src/app/api/native/git/knowledge-files/route.ts + +'use server'; +import { NextResponse } from 'next/server'; +import * as git from 'isomorphic-git'; +import fs from 'fs'; +import path from 'path'; +import { cloneTaxonomyDocsRepo, findTaxonomyDocRepoPath, TAXONOMY_DOCS_ROOT_DIR } from '@/app/api/native/git/utils'; + +/** + * GET handler to retrieve knowledge files from the taxonomy-knowledge-doc main branch. + */ +export async function GET() { + try { + const docsRepoUrl = await cloneTaxonomyDocsRepo(); + const REPO_DIR = findTaxonomyDocRepoPath(); + + // If the repository was not cloned, return an error + if (!docsRepoUrl) { + return NextResponse.json({ error: 'Failed to clone taxonomy knowledge docs repository' }, { status: 500 }); + } + + let commitSha: string = ''; + + // Checkout the main branch + await git.checkout({ + fs, + dir: docsRepoUrl, + ref: 'main', + onPostCheckout: (data) => { + commitSha = data.newHead; + } + }); + + // Read all files in the repository root directory + const allFiles = fs.readdirSync(REPO_DIR); + + // Filter for Markdown files only + const markdownFiles = allFiles.filter((file) => path.extname(file).toLowerCase() === '.md'); + + const knowledgeFiles: { + filename: string; + commitSha: string; + }[] = []; + let mostRecentSha = ''; + let mostRecentDate = 0; + + for (const file of markdownFiles) { + const filePath = path.join(REPO_DIR, file); + + // Check if the file is a regular file + const stat = fs.statSync(filePath); + if (!stat.isFile()) { + continue; + } + + try { + // Retrieve the latest commit SHA for the file on the specified branch + const logs = await git.log({ + fs, + dir: REPO_DIR, + ref: 'main', + filepath: file, + depth: 1 // Only the latest commit + }); + + if (logs.length === 0) { + // No commits found for this file; skip it + continue; + } + + const latestCommit = logs[0]; + const commitSha = latestCommit.oid; + const commitDate = latestCommit.commit.committer.timestamp; + + if (commitDate > mostRecentDate) { + mostRecentDate = commitDate; + mostRecentSha = commitSha; + } + + knowledgeFiles.push({ filename: file, commitSha: commitSha }); + } catch (error) { + console.error(`Failed to retrieve commit for file ${file}:`, error); + } + } + + const fileNames = knowledgeFiles + .filter((knowledgeFile) => knowledgeFile.commitSha === mostRecentSha) + .map((knowledgeFile) => knowledgeFile.filename) + .join(','); + const origTaxonomyDocsRepoDir = path.join(TAXONOMY_DOCS_ROOT_DIR, '/taxonomy-knowledge-docs'); + + return NextResponse.json( + { + repoUrl: origTaxonomyDocsRepoDir, + commitSha, + fileNames + }, + { status: 201 } + ); + } catch (error) { + console.error('Failed to upload knowledge documents:', error); + return NextResponse.json({ error: 'Failed to upload knowledge documents' }, { status: 500 }); + } +} diff --git a/src/app/api/native/git/knowledge-files/route.ts b/src/app/api/native/git/knowledge-files/route.ts index 55d1626c..7b54eeb3 100644 --- a/src/app/api/native/git/knowledge-files/route.ts +++ b/src/app/api/native/git/knowledge-files/route.ts @@ -5,13 +5,8 @@ import { NextRequest, NextResponse } from 'next/server'; import * as git from 'isomorphic-git'; import fs from 'fs'; import path from 'path'; -import http from 'isomorphic-git/http/node'; +import { cloneTaxonomyDocsRepo, findTaxonomyDocRepoPath, TAXONOMY_DOCS_ROOT_DIR } from '@/app/api/native/git/utils'; -// Constants for repository paths -const TAXONOMY_DOCS_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || ''; -const TAXONOMY_DOCS_CONTAINER_MOUNT_DIR = '/tmp/.instructlab-ui'; -const TAXONOMY_KNOWLEDGE_DOCS_REPO_URL = - process.env.NEXT_PUBLIC_TAXONOMY_DOCUMENTS_REPO || 'https://github.com/instructlab-public/taxonomy-knowledge-docs'; const BASE_BRANCH = 'main'; // Interface for the response @@ -22,27 +17,6 @@ interface KnowledgeFile { commitDate: string; } -function findTaxonomyDocRepoPath(): string { - // Check the location of the taxonomy docs repository . - let remoteTaxonomyDocsRepoDirFinal: string = ''; - // Check if the taxonomy docs repo directory is mounted in the container (for container deployment) or present locally (for local deployment). - const remoteTaxonomyDocsRepoContainerMountDir = path.join(TAXONOMY_DOCS_CONTAINER_MOUNT_DIR, '/taxonomy-knowledge-docs'); - const remoteTaxonomyDocsRepoDir = path.join(TAXONOMY_DOCS_ROOT_DIR, '/taxonomy-knowledge-docs'); - if (fs.existsSync(remoteTaxonomyDocsRepoContainerMountDir) && fs.readdirSync(remoteTaxonomyDocsRepoContainerMountDir).length !== 0) { - remoteTaxonomyDocsRepoDirFinal = TAXONOMY_DOCS_CONTAINER_MOUNT_DIR; - } else { - if (fs.existsSync(remoteTaxonomyDocsRepoDir) && fs.readdirSync(remoteTaxonomyDocsRepoDir).length !== 0) { - remoteTaxonomyDocsRepoDirFinal = TAXONOMY_DOCS_ROOT_DIR; - } - } - if (remoteTaxonomyDocsRepoDirFinal === '') { - return ''; - } - - const taxonomyDocsDirectoryPath = path.join(remoteTaxonomyDocsRepoDirFinal, '/taxonomy-knowledge-docs'); - return taxonomyDocsDirectoryPath; -} - /** * Function to retrieve knowledge files from a specific branch. * @param branchName - The name of the branch to retrieve files from. @@ -203,50 +177,3 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'Failed to upload knowledge documents' }, { status: 500 }); } } - -async function cloneTaxonomyDocsRepo() { - // Check the location of the taxonomy repository and create the taxonomy-knowledge-doc repository parallel to that. - let remoteTaxonomyRepoDirFinal: string = ''; - // Check if directory pointed by remoteTaxonomyRepoDir exists and not empty - const remoteTaxonomyRepoContainerMountDir = path.join(TAXONOMY_DOCS_CONTAINER_MOUNT_DIR, '/taxonomy'); - const remoteTaxonomyRepoDir = path.join(TAXONOMY_DOCS_ROOT_DIR, '/taxonomy'); - if (fs.existsSync(remoteTaxonomyRepoContainerMountDir) && fs.readdirSync(remoteTaxonomyRepoContainerMountDir).length !== 0) { - remoteTaxonomyRepoDirFinal = TAXONOMY_DOCS_CONTAINER_MOUNT_DIR; - } else { - if (fs.existsSync(remoteTaxonomyRepoDir) && fs.readdirSync(remoteTaxonomyRepoDir).length !== 0) { - remoteTaxonomyRepoDirFinal = TAXONOMY_DOCS_ROOT_DIR; - } - } - if (remoteTaxonomyRepoDirFinal === '') { - return null; - } - - const taxonomyDocsDirectoryPath = path.join(remoteTaxonomyRepoDirFinal, '/taxonomy-knowledge-docs'); - - if (fs.existsSync(taxonomyDocsDirectoryPath)) { - console.log(`Using existing taxonomy knowledge docs repository at ${TAXONOMY_DOCS_ROOT_DIR}/taxonomy-knowledge-docs.`); - return taxonomyDocsDirectoryPath; - } else { - console.log(`Taxonomy knowledge docs repository not found at ${TAXONOMY_DOCS_ROOT_DIR}/taxonomy-knowledge-docs. Cloning...`); - } - - try { - await git.clone({ - fs, - http, - dir: taxonomyDocsDirectoryPath, - url: TAXONOMY_KNOWLEDGE_DOCS_REPO_URL, - singleBranch: true - }); - - // Include the full path in the response for client display. Path displayed here is the one - // that user set in the environment variable. - console.log(`Taxonomy knowledge docs repository cloned successfully to ${remoteTaxonomyRepoDir}.`); - // Return the path that the UI sees (direct or mounted) - return taxonomyDocsDirectoryPath; - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - console.error(`Failed to clone taxonomy docs repository: ${errorMessage}`); - return null; - } -} diff --git a/src/app/api/native/git/utils.ts b/src/app/api/native/git/utils.ts new file mode 100644 index 00000000..7f7867cd --- /dev/null +++ b/src/app/api/native/git/utils.ts @@ -0,0 +1,78 @@ +// Constants for repository paths +import path from 'path'; +import fs from 'fs'; +import * as git from 'isomorphic-git'; +import http from 'isomorphic-git/http/node'; + +export const TAXONOMY_DOCS_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || ''; +export const TAXONOMY_DOCS_CONTAINER_MOUNT_DIR = '/tmp/.instructlab-ui'; +export const TAXONOMY_KNOWLEDGE_DOCS_REPO_URL = + process.env.NEXT_PUBLIC_TAXONOMY_DOCUMENTS_REPO || 'https://github.com/instructlab-public/taxonomy-knowledge-docs'; + +export const cloneTaxonomyDocsRepo = async (): Promise => { + // Check the location of the taxonomy repository and create the taxonomy-knowledge-doc repository parallel to that. + let remoteTaxonomyRepoDirFinal: string = ''; + // Check if directory pointed by remoteTaxonomyRepoDir exists and not empty + const remoteTaxonomyRepoContainerMountDir = path.join(TAXONOMY_DOCS_CONTAINER_MOUNT_DIR, '/taxonomy'); + const remoteTaxonomyRepoDir = path.join(TAXONOMY_DOCS_ROOT_DIR, '/taxonomy'); + if (fs.existsSync(remoteTaxonomyRepoContainerMountDir) && fs.readdirSync(remoteTaxonomyRepoContainerMountDir).length !== 0) { + remoteTaxonomyRepoDirFinal = TAXONOMY_DOCS_CONTAINER_MOUNT_DIR; + } else { + if (fs.existsSync(remoteTaxonomyRepoDir) && fs.readdirSync(remoteTaxonomyRepoDir).length !== 0) { + remoteTaxonomyRepoDirFinal = TAXONOMY_DOCS_ROOT_DIR; + } + } + if (remoteTaxonomyRepoDirFinal === '') { + return null; + } + + const taxonomyDocsDirectoryPath = path.join(remoteTaxonomyRepoDirFinal, '/taxonomy-knowledge-docs'); + + if (fs.existsSync(taxonomyDocsDirectoryPath)) { + console.log(`Using existing taxonomy knowledge docs repository at ${TAXONOMY_DOCS_ROOT_DIR}/taxonomy-knowledge-docs.`); + return taxonomyDocsDirectoryPath; + } else { + console.log(`Taxonomy knowledge docs repository not found at ${TAXONOMY_DOCS_ROOT_DIR}/taxonomy-knowledge-docs. Cloning...`); + } + + try { + await git.clone({ + fs, + http, + dir: taxonomyDocsDirectoryPath, + url: TAXONOMY_KNOWLEDGE_DOCS_REPO_URL, + singleBranch: true + }); + + // Include the full path in the response for client display. Path displayed here is the one + // that user set in the environment variable. + console.log(`Taxonomy knowledge docs repository cloned successfully to ${remoteTaxonomyRepoDir}.`); + // Return the path that the UI sees (direct or mounted) + return taxonomyDocsDirectoryPath; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + console.error(`Failed to clone taxonomy docs repository: ${errorMessage}`); + return null; + } +}; + +export const findTaxonomyDocRepoPath = (): string => { + // Check the location of the taxonomy docs repository . + let remoteTaxonomyDocsRepoDirFinal: string = ''; + // Check if the taxonomy docs repo directory is mounted in the container (for container deployment) or present locally (for local deployment). + const remoteTaxonomyDocsRepoContainerMountDir = path.join(TAXONOMY_DOCS_CONTAINER_MOUNT_DIR, '/taxonomy-knowledge-docs'); + const remoteTaxonomyDocsRepoDir = path.join(TAXONOMY_DOCS_ROOT_DIR, '/taxonomy-knowledge-docs'); + if (fs.existsSync(remoteTaxonomyDocsRepoContainerMountDir) && fs.readdirSync(remoteTaxonomyDocsRepoContainerMountDir).length !== 0) { + remoteTaxonomyDocsRepoDirFinal = TAXONOMY_DOCS_CONTAINER_MOUNT_DIR; + } else { + if (fs.existsSync(remoteTaxonomyDocsRepoDir) && fs.readdirSync(remoteTaxonomyDocsRepoDir).length !== 0) { + remoteTaxonomyDocsRepoDirFinal = TAXONOMY_DOCS_ROOT_DIR; + } + } + if (remoteTaxonomyDocsRepoDirFinal === '') { + return ''; + } + + const taxonomyDocsDirectoryPath = path.join(remoteTaxonomyDocsRepoDirFinal, '/taxonomy-knowledge-docs'); + return taxonomyDocsDirectoryPath; +}; diff --git a/src/app/contribute/knowledge/page.tsx b/src/app/contribute/knowledge/page.tsx index 063a0234..f86119a5 100644 --- a/src/app/contribute/knowledge/page.tsx +++ b/src/app/contribute/knowledge/page.tsx @@ -1,23 +1,46 @@ // src/app/contribute/knowledge/page.tsx 'use client'; -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { Flex, Spinner } from '@patternfly/react-core'; +import { t_global_spacer_xl as XlSpacerSize } from '@patternfly/react-tokens'; import { AppLayout } from '@/components/AppLayout'; import KnowledgeFormGithub from '@/components/Contribute/Knowledge/Github'; import KnowledgeFormNative from '@/components/Contribute/Knowledge/Native'; const KnowledgeFormPage: React.FunctionComponent = () => { const [deploymentType, setDeploymentType] = useState(); + const [loaded, setLoaded] = useState(); useEffect(() => { + let canceled = false; + const getEnvVariables = async () => { const res = await fetch('/api/envConfig'); const envConfig = await res.json(); - setDeploymentType(envConfig.DEPLOYMENT_TYPE); + if (!canceled) { + setDeploymentType(envConfig.DEPLOYMENT_TYPE); + setLoaded(true); + } }; + getEnvVariables(); + + return () => { + canceled = true; + }; }, []); - return {deploymentType === 'native' ? : }; + return ( + + {loaded ? ( + <>{deploymentType === 'native' ? : } + ) : ( + + + + )} + + ); }; export default KnowledgeFormPage; diff --git a/src/components/Common/WizardFormGroupLabelHelp.tsx b/src/components/Common/WizardFormGroupLabelHelp.tsx new file mode 100644 index 00000000..4d518bc6 --- /dev/null +++ b/src/components/Common/WizardFormGroupLabelHelp.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { FormGroupLabelHelp, Popover } from '@patternfly/react-core'; + +interface Props { + headerContent?: React.ReactNode; + bodyContent: React.ReactNode; + ariaLabel?: string; +} + +const WizardFormGroupLabelHelp: React.FC = ({ headerContent, bodyContent, ariaLabel = 'More info' }) => { + const labelHelpRef = React.useRef(null); + + return ( + + + + ); +}; + +export default WizardFormGroupLabelHelp; diff --git a/src/components/Contribute/PageHeader.tsx b/src/components/Common/WizardPageHeader.tsx similarity index 64% rename from src/components/Contribute/PageHeader.tsx rename to src/components/Common/WizardPageHeader.tsx index 6663417a..9a2f218d 100644 --- a/src/components/Contribute/PageHeader.tsx +++ b/src/components/Common/WizardPageHeader.tsx @@ -3,14 +3,14 @@ import { Content } from '@patternfly/react-core'; interface Props { title: React.ReactNode; - description: React.ReactNode; + description?: React.ReactNode; } -const PageHeader: React.FC = ({ title, description }) => ( +const WizardPageHeader: React.FC = ({ title, description }) => (
{title} {description}
); -export default PageHeader; +export default WizardPageHeader; diff --git a/src/components/Common/WizardSectionHeader.tsx b/src/components/Common/WizardSectionHeader.tsx new file mode 100644 index 00000000..137ba7f6 --- /dev/null +++ b/src/components/Common/WizardSectionHeader.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Content, Popover, Button } from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; +import { t_global_spacer_sm as SmallSpacerSize } from '@patternfly/react-tokens'; + +interface Props { + title: string; + description?: string; + helpInfo?: React.ReactNode; +} +const WizardSectionHeader: React.FC = ({ title, description, helpInfo }) => ( + <> + + {title} + {helpInfo ? ( + <> + {' '} + +