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/pr/knowledge/route.ts b/src/app/api/github/pr/knowledge/route.ts index baa727ce..ef4f7d33 100644 --- a/src/app/api/github/pr/knowledge/route.ts +++ b/src/app/api/github/pr/knowledge/route.ts @@ -6,6 +6,7 @@ import { KnowledgeYamlData, AttributionData } from '@/types'; import { GITHUB_API_URL, BASE_BRANCH } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; import { checkUserForkExists, createBranch, createFilesInSingleCommit, createFork, getBaseBranchSha, getGitHubUsername } from '@/utils/github'; +import { prInfoFromSummary } from '@/app/api/github/utils'; const KNOWLEDGE_DIR = 'knowledge'; const UPSTREAM_REPO_OWNER = process.env.NEXT_PUBLIC_TAXONOMY_REPO_OWNER!; @@ -29,7 +30,7 @@ export async function POST(req: NextRequest) { try { const body = await req.json(); - const { content, attribution, name, email, submissionSummary, documentOutline, filePath } = body; + const { content, attribution, name, email, submissionSummary, filePath } = body; const knowledgeData: KnowledgeYamlData = yaml.load(content) as KnowledgeYamlData; const attributionData: AttributionData = attribution; @@ -64,6 +65,8 @@ Creator names: ${attributionData.creator_names} // Create a new branch in the user's fork await createBranch(headers, githubUsername, UPSTREAM_REPO_NAME, branchName, baseBranchSha); + const { prTitle, prBody, commitMessage } = prInfoFromSummary(submissionSummary); + // Create both files in a single commit with DCO sign-off await createFilesInSingleCommit( headers, @@ -74,11 +77,11 @@ Creator names: ${attributionData.creator_names} { path: newAttributionFilePath, content: attributionContent } ], branchName, - `${submissionSummary}\n\nSigned-off-by: ${name} <${email}>` + `${commitMessage}\n\nSigned-off-by: ${name} <${email}>` ); // Create a pull request from the user's fork to the upstream repository - const pr = await createPullRequest(headers, githubUsername, branchName, submissionSummary, documentOutline); + const pr = await createPullRequest(headers, githubUsername, branchName, prTitle, prBody); return NextResponse.json(pr, { status: 201 }); } catch (error) { @@ -87,14 +90,14 @@ Creator names: ${attributionData.creator_names} } } -async function createPullRequest(headers: HeadersInit, username: string, branchName: string, knowledgeSummary: string, documentOutline: string) { +async function createPullRequest(headers: HeadersInit, username: string, branchName: string, prTitle: string, prBody?: string) { const response = await fetch(`${GITHUB_API_URL}/repos/${UPSTREAM_REPO_OWNER}/${UPSTREAM_REPO_NAME}/pulls`, { method: 'POST', headers, body: JSON.stringify({ - title: `Knowledge: ${knowledgeSummary}`, + title: `Knowledge: ${prTitle}`, head: `${username}:${branchName}`, - body: documentOutline, + body: prBody, base: BASE_BRANCH }) }); diff --git a/src/app/api/github/pr/skill/route.ts b/src/app/api/github/pr/skill/route.ts index 380a8f8b..b07dc820 100644 --- a/src/app/api/github/pr/skill/route.ts +++ b/src/app/api/github/pr/skill/route.ts @@ -6,6 +6,7 @@ import { SkillYamlData, AttributionData } from '@/types'; import { GITHUB_API_URL, BASE_BRANCH } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; import { checkUserForkExists, createBranch, createFilesInSingleCommit, createFork, getBaseBranchSha, getGitHubUsername } from '@/utils/github'; +import { prInfoFromSummary } from '@/app/api/github/utils'; const SKILLS_DIR = 'compositional_skills'; const UPSTREAM_REPO_OWNER = process.env.NEXT_PUBLIC_TAXONOMY_REPO_OWNER!; @@ -29,7 +30,7 @@ export async function POST(req: NextRequest) { try { const body = await req.json(); - const { content, attribution, name, email, submissionSummary, documentOutline, filePath } = body; + const { content, attribution, name, email, submissionSummary, filePath } = body; const githubUsername = await getGitHubUsername(headers); console.log('Skill contribution from gitHub Username:', githubUsername); @@ -62,6 +63,8 @@ Creator names: ${attributionData.creator_names} // Create a new branch in the user's fork await createBranch(headers, githubUsername, UPSTREAM_REPO_NAME, branchName, baseBranchSha); + const { prTitle, prBody, commitMessage } = prInfoFromSummary(submissionSummary); + // Create both files in a single commit await createFilesInSingleCommit( headers, @@ -72,11 +75,11 @@ Creator names: ${attributionData.creator_names} { path: newAttributionFilePath, content: attributionString } ], branchName, - `${submissionSummary}\n\nSigned-off-by: ${name} <${email}>` + `${commitMessage}\n\nSigned-off-by: ${name} <${email}>` ); // Create a pull request from the user's fork to the upstream repository - const pr = await createPullRequest(headers, githubUsername, branchName, submissionSummary, documentOutline); + const pr = await createPullRequest(headers, githubUsername, branchName, prTitle, prBody); return NextResponse.json(pr, { status: 201 }); } catch (error) { @@ -85,14 +88,14 @@ Creator names: ${attributionData.creator_names} } } -async function createPullRequest(headers: HeadersInit, username: string, branchName: string, skillSummary: string, skillDescription: string) { +async function createPullRequest(headers: HeadersInit, username: string, branchName: string, prTitle: string, prBody?: string) { const response = await fetch(`${GITHUB_API_URL}/repos/${UPSTREAM_REPO_OWNER}/${UPSTREAM_REPO_NAME}/pulls`, { method: 'POST', headers, body: JSON.stringify({ - title: `Skill: ${skillSummary}`, + title: `Skill: ${prTitle}`, + body: prBody, head: `${username}:${branchName}`, - body: skillDescription, base: BASE_BRANCH }) }); diff --git a/src/app/api/github/utils.ts b/src/app/api/github/utils.ts new file mode 100644 index 00000000..2adb329e --- /dev/null +++ b/src/app/api/github/utils.ts @@ -0,0 +1,121 @@ +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 prInfoFromSummary = (summaryString: string): { prTitle: string; prBody?: string; commitMessage: string } => { + const prTitle = summaryString.length > 60 ? `${summaryString.slice(0, 60)}...` : summaryString; + const prBody = summaryString.length > 60 ? `...${summaryString.slice(60)}` : undefined; + const commitMessage = `${prTitle}${prBody ? `\n\n${prBody}` : ''}`; + return { prTitle, prBody, commitMessage }; +}; + +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/convert/route.ts b/src/app/api/native/convert/route.ts index 88dc43da..9d7678e9 100644 --- a/src/app/api/native/convert/route.ts +++ b/src/app/api/native/convert/route.ts @@ -16,14 +16,14 @@ interface ConvertRequestBody { // This route calls the external REST service to convert any doc => markdown export async function POST(request: Request) { - try { - // 1. Parse JSON body from client - const body: ConvertRequestBody = await request.json(); + // 1. Parse JSON body from client + const body: ConvertRequestBody = await request.json(); - // 2. Read the IL_FILE_CONVERSION_SERVICE from .env - const baseUrl = process.env.IL_FILE_CONVERSION_SERVICE || 'http://doclingserve:5001'; + // 2. Read the IL_FILE_CONVERSION_SERVICE from .env + const baseUrl = process.env.IL_FILE_CONVERSION_SERVICE || 'http://doclingserve:5001'; - // 3. Check the health of the conversion service before proceeding + // 3. Check the health of the conversion service before proceeding + try { 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); @@ -36,8 +36,13 @@ export async function POST(request: Request) { console.error('Doc->md 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 }); + } - // 4. Service is healthy, proceed with md conversion + // 4. Service is healthy, proceed with md conversion + try { const res = await fetch(`${baseUrl}/v1alpha/convert/source`, { method: 'POST', headers: { 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/api/native/pr/knowledge/route.ts b/src/app/api/native/pr/knowledge/route.ts index 6a2c9363..372b9cbf 100644 --- a/src/app/api/native/pr/knowledge/route.ts +++ b/src/app/api/native/pr/knowledge/route.ts @@ -8,6 +8,7 @@ import path from 'path'; import { dumpYaml } from '@/utils/yamlConfig'; import { KnowledgeYamlData } from '@/types'; import yaml from 'js-yaml'; +import { prInfoFromSummary } from '@/app/api/github/utils'; // Define paths and configuration const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; @@ -74,10 +75,11 @@ export async function POST(req: NextRequest) { } // Commit the changes + const { commitMessage } = prInfoFromSummary(submissionSummary); await git.commit({ fs, dir: REPO_DIR, - message: `${submissionSummary}\n\nSigned-off-by: ${name} <${email}>`, + message: `${commitMessage}\n\nSigned-off-by: ${name} <${email}>`, author: { name: name, email: email diff --git a/src/app/api/native/pr/skill/route.ts b/src/app/api/native/pr/skill/route.ts index f4446e9e..1dbc789b 100644 --- a/src/app/api/native/pr/skill/route.ts +++ b/src/app/api/native/pr/skill/route.ts @@ -7,6 +7,7 @@ import path from 'path'; import yaml from 'js-yaml'; import { SkillYamlData } from '@/types'; import { dumpYaml } from '@/utils/yamlConfig'; +import { prInfoFromSummary } from '@/app/api/github/utils'; // Define paths and configuration const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; @@ -16,8 +17,8 @@ const SKILLS_DIR = 'compositional_skills'; export async function POST(req: NextRequest) { const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); try { - // Extract the QnA data from the request body TODO: what is documentOutline? - const { action, branchName, content, name, email, submissionSummary, documentOutline, filePath, oldFilesPath } = await req.json(); // eslint-disable-line @typescript-eslint/no-unused-vars + // Extract the QnA data from the request body + const { action, branchName, content, name, email, submissionSummary, filePath, oldFilesPath } = await req.json(); // eslint-disable-line @typescript-eslint/no-unused-vars let skillBranchName; if (action == 'update' && branchName != '') { @@ -71,10 +72,11 @@ export async function POST(req: NextRequest) { } // Commit files + const { commitMessage } = prInfoFromSummary(submissionSummary); await git.commit({ fs, dir: REPO_DIR, - message: `${submissionSummary}\n\nSigned-off-by: ${name} <${email}>`, + message: `${commitMessage}\n\nSigned-off-by: ${name} <${email}>`, author: { name: name, email: email diff --git a/src/app/contribute/contribute-page.scss b/src/app/contribute/contribute-page.scss new file mode 100644 index 00000000..c3bc2902 --- /dev/null +++ b/src/app/contribute/contribute-page.scss @@ -0,0 +1,10 @@ +.contribute-page { + .pf-v6-c-page__main { + overflow-y: hidden; + } + .pf-v6-c-page__main-section.pf-m-fill { + .pf-v6-c-page__main-body { + height: 100%; + } + } +} diff --git a/src/app/contribute/knowledge/page.tsx b/src/app/contribute/knowledge/page.tsx index 063a0234..cc8efbb4 100644 --- a/src/app/contribute/knowledge/page.tsx +++ b/src/app/contribute/knowledge/page.tsx @@ -1,23 +1,48 @@ // 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'; +import '../contribute-page.scss'; + 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/app/contribute/skill/page.tsx b/src/app/contribute/skill/page.tsx index b4d31d35..65ee741f 100644 --- a/src/app/contribute/skill/page.tsx +++ b/src/app/contribute/skill/page.tsx @@ -5,6 +5,8 @@ import { SkillFormGithub } from '@/components/Contribute/Skill/Github/index'; import { SkillFormNative } from '@/components/Contribute/Skill/Native/index'; import { useEffect, useState } from 'react'; +import '../contribute-page.scss'; + const SkillFormPage: React.FunctionComponent = () => { const [deploymentType, setDeploymentType] = useState(); @@ -17,7 +19,7 @@ const SkillFormPage: React.FunctionComponent = () => { getEnvVariables(); }, []); - return {deploymentType === 'native' ? : }; + return {deploymentType === 'native' ? : }; }; export default SkillFormPage; diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 0a2ad3e4..c61299ff 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -185,7 +185,15 @@ const AppLayout: React.FunctionComponent = ({ children, className }) const PageSkipToContent = Skip to Content; return ( - + {children} ); diff --git a/src/components/Common/WizardFormGroupLabelHelp.tsx b/src/components/Common/WizardFormGroupLabelHelp.tsx new file mode 100644 index 00000000..c0c5132c --- /dev/null +++ b/src/components/Common/WizardFormGroupLabelHelp.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { Button, Popover } from '@patternfly/react-core'; +import { t_global_font_size_xs as XsFontSize, t_global_icon_color_subtle as SubtleIconColor } from '@patternfly/react-tokens'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; + +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..c548c7f7 --- /dev/null +++ b/src/components/Common/WizardSectionHeader.tsx @@ -0,0 +1,38 @@ +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, + t_global_font_size_xs as XsFontSize, + t_global_icon_color_subtle as SubtleIconColor +} from '@patternfly/react-tokens'; + +interface Props { + title: string; + description?: string; + helpInfo?: React.ReactNode; +} +const WizardSectionHeader: React.FC = ({ title, description, helpInfo }) => ( + <> + + {title} + {helpInfo ? ( + <> + {' '} + + + + + ) : null} + + {description ? ( + + {description} + + ) : null} + +); + +export default WizardSectionHeader; diff --git a/src/components/Contribute/AttributionInformation/AttributionInformation.tsx b/src/components/Contribute/AttributionInformation/AttributionInformation.tsx index e36c680d..13aa434f 100644 --- a/src/components/Contribute/AttributionInformation/AttributionInformation.tsx +++ b/src/components/Contribute/AttributionInformation/AttributionInformation.tsx @@ -2,7 +2,8 @@ import React, { useEffect } from 'react'; import { ContributionFormData } from '@/types'; import { ValidatedOptions, FormGroup, TextInput, FormHelperText, HelperText, HelperTextItem, FlexItem, Flex, Form } from '@patternfly/react-core'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; -import PageHeader from '@/components/Contribute/PageHeader'; +import WizardPageHeader from '@/components/Common/WizardPageHeader'; +import WizardFormGroupLabelHelp from '@/components/Common/WizardFormGroupLabelHelp'; interface Props { isEditForm?: boolean; @@ -91,17 +92,46 @@ const AttributionInformation: React.FC = ({ return ( - +
+ } + > + setTitleWork(value)} + onBlur={() => validateTitle(titleWork)} + /> + {validTitle === ValidatedOptions.error && ( + + + } variant={validTitle}> + Required field + + + + )} + {setLinkWork ? ( - + } + > setLinkWork(value)} @@ -127,34 +157,17 @@ const AttributionInformation: React.FC = ({ )} ) : null} - - setTitleWork(value)} - onBlur={() => validateTitle(titleWork)} - /> - {validTitle === ValidatedOptions.error && ( - - - } variant={validTitle}> - Required field - - - - )} - {setRevision ? ( - + } + > setRevision(value)} @@ -171,12 +184,16 @@ const AttributionInformation: React.FC = ({ )} ) : null} - + } + > setLicenseWork(value)} @@ -192,12 +209,11 @@ const AttributionInformation: React.FC = ({ )} - + setCreators(value)} diff --git a/src/components/Contribute/AuthorInformation/AuthorInformation.tsx b/src/components/Contribute/AuthorInformation/AuthorInformation.tsx deleted file mode 100644 index e03c8319..00000000 --- a/src/components/Contribute/AuthorInformation/AuthorInformation.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import { ValidatedOptions, FormGroup, TextInput, FormHelperText, HelperText, HelperTextItem, Form, Flex, FlexItem } from '@patternfly/react-core'; -import { ExclamationCircleIcon } from '@patternfly/react-icons'; -import { isEmailValid } from '@/components/Contribute/Utils/validationUtils'; -import PageHeader from '@/components/Contribute/PageHeader'; - -interface Props { - email: string; - setEmail: (val: string) => void; - name: string; - setName: (val: string) => void; -} -const AuthorInformation: React.FC = ({ email, setEmail, name, setName }) => { - const [validEmail, setValidEmail] = React.useState(ValidatedOptions.default); - const [validName, setValidName] = React.useState(ValidatedOptions.default); - const [validEmailError, setValidEmailError] = React.useState('Required Field'); - const touchedRef = React.useRef(); - - const validateEmail = (emailStr: string) => { - const email = emailStr.trim(); - if (isEmailValid(email)) { - setValidEmail(ValidatedOptions.success); - setValidEmailError(''); - return; - } - const errMsg = email ? 'Please enter a valid email address.' : 'Required field'; - setValidEmail(ValidatedOptions.error); - setValidEmailError(errMsg); - }; - - const validateName = (nameStr: string) => { - const name = nameStr.trim(); - setValidName(name.length > 0 ? ValidatedOptions.success : ValidatedOptions.error); - }; - - return ( - - - - - - - - { - touchedRef.current = true; - setEmail(value); - }} - onBlur={() => validateEmail(email)} - /> - {validEmail === ValidatedOptions.error && ( - - - } variant={validEmail}> - {validEmailError} - - - - )} - - - setName(value)} - onBlur={() => validateName(name)} - /> - {validName === ValidatedOptions.error && ( - - - } variant={validName}> - Required field - - - - )} - - - - - ); -}; - -export default AuthorInformation; diff --git a/src/components/Contribute/AutoFill.ts b/src/components/Contribute/AutoFill.ts index 536e38da..383dde6f 100644 --- a/src/components/Contribute/AutoFill.ts +++ b/src/components/Contribute/AutoFill.ts @@ -254,9 +254,6 @@ export const autoFillKnowledgeFields: KnowledgeFormData = { email: 'helloworld@instructlab.com', name: 'juliadenham', submissionSummary: 'Information about the Phoenix Constellation.', - domain: 'astronomy', - documentOutline: - 'Information about the Phoenix Constellation including the history, characteristics, and features of the stars in the constellation.', filePath: 'science/physics/astrophysics/stars', seedExamples: knowledgeSeedExamples, knowledgeDocumentRepositoryUrl: '~/.instructlab-ui/taxonomy-knowledge-docs', @@ -266,7 +263,8 @@ export const autoFillKnowledgeFields: KnowledgeFormData = { linkWork: 'https://en.wikipedia.org/wiki/Phoenix_(constellation)', revision: 'https://en.wikipedia.org/w/index.php?title=Phoenix_(constellation)&oldid=1237187773', licenseWork: 'CC-BY-SA-4.0', - creators: 'Wikipedia Authors' + creators: 'Wikipedia Authors', + filesToUpload: [] }; const skillsSeedExamples: SkillSeedExample[] = [ @@ -356,7 +354,6 @@ export const autoFillSkillsFields: SkillFormData = { email: 'helloworld@instructlab.com', name: 'juliadenham', submissionSummary: 'Teaching a model to rhyme.', - documentOutline: 'These provided examples demonstrate how to rhyme.', filePath: 'science/physics/astrophysics/stars', seedExamples: skillsSeedExamples, titleWork: 'Teaching a model to rhyme.', diff --git a/src/components/Contribute/ContributeAlertGroup.tsx b/src/components/Contribute/ContributeAlertGroup.tsx index f5fdeaeb..9e8bd472 100644 --- a/src/components/Contribute/ContributeAlertGroup.tsx +++ b/src/components/Contribute/ContributeAlertGroup.tsx @@ -17,45 +17,47 @@ export const ContributeAlertGroup: React.FunctionComponent - } - > - - - - {actionGroupAlertContent.waitAlert ? ( - - - - ) : null} - {actionGroupAlertContent.message} - - - {!actionGroupAlertContent.waitAlert && - actionGroupAlertContent.success && - actionGroupAlertContent.url && - actionGroupAlertContent.url.trim().length > 0 ? ( + {actionGroupAlertContent ? ( + } + > + - + + {actionGroupAlertContent.waitAlert ? ( + + + + ) : null} + {actionGroupAlertContent.message} + - ) : null} - - + {!actionGroupAlertContent.waitAlert && + actionGroupAlertContent.success && + actionGroupAlertContent.url && + actionGroupAlertContent.url.trim().length > 0 ? ( + + + + ) : null} + + + ) : null} ); }; diff --git a/src/components/Contribute/ContributionWizard/ContributionWizard.tsx b/src/components/Contribute/ContributionWizard/ContributionWizard.tsx index b79f9da4..a870562c 100644 --- a/src/components/Contribute/ContributionWizard/ContributionWizard.tsx +++ b/src/components/Contribute/ContributionWizard/ContributionWizard.tsx @@ -85,6 +85,7 @@ export const ContributionWizard: React.FunctionComponent = ({ }, []), [steps] ); + const getStepIndex = (stepId: string) => stepIds.indexOf(stepId); React.useEffect(() => { const getEnvVariables = async () => { @@ -149,15 +150,14 @@ export const ContributionWizard: React.FunctionComponent = ({ }, [steps]); return ( - - + + Contribute {title} - - + @@ -178,41 +178,54 @@ export const ContributionWizard: React.FunctionComponent = ({ {description} - - setActiveStepIndex(stepIds.indexOf(String(currentStep.id)))} - footer={ - onSubmit(githubUsername)} - isValid={true} - showSubmit={submitEnabled} - isEdit={!!editFormData} - convertToYaml={convertToYaml} - /> + + + + setActiveStepIndex(stepIds.indexOf(String(currentStep.id)))} + footer={ + onSubmit(githubUsername)} + showSubmit={submitEnabled} + isEdit={!!editFormData} + convertToYaml={convertToYaml} + /> + } + > + {steps.map((step) => ( + {step.name} }} + status={getStepIndex(step.id) < activeStepIndex ? step.status : StepStatus.Default} + steps={ + step.subSteps + ? step.subSteps.map((subStep) => ( + {subStep.name} }} + status={getStepIndex(subStep.id) < activeStepIndex ? subStep.status : StepStatus.Default} + > + {subStep.component} + + )) + : undefined } > - {steps.map((step, index) => ( - activeStepIndex) ? StepStatus.Default : step.status - } - > - {step.component} - - ))} - -
-
+ {!step.subSteps ? step.component : null} + + ))} + ); diff --git a/src/components/Contribute/ContributionWizard/ContributionWizardFooter.tsx b/src/components/Contribute/ContributionWizard/ContributionWizardFooter.tsx index a0efb47e..9fc5382b 100644 --- a/src/components/Contribute/ContributionWizard/ContributionWizardFooter.tsx +++ b/src/components/Contribute/ContributionWizard/ContributionWizardFooter.tsx @@ -15,7 +15,6 @@ import ViewDropdownButton from '@/components/Contribute/ContributionWizard/ViewD import { ContributionFormData } from '@/types'; interface Props { - isValid: boolean; formData: ContributionFormData; isGithubMode: boolean; isSkillContribution: boolean; @@ -26,16 +25,7 @@ interface Props { onCancel: () => void; } -const ContributionWizardFooter: React.FC = ({ - isValid, - formData, - isGithubMode, - isSkillContribution, - showSubmit, - onSubmit, - convertToYaml, - isEdit -}) => { +const ContributionWizardFooter: React.FC = ({ formData, isGithubMode, isSkillContribution, showSubmit, onSubmit, convertToYaml, isEdit }) => { const { steps, activeStep, goToNextStep, goToPrevStep, goToStepByIndex, close } = useWizardContext(); const prevDisabled = steps.indexOf(activeStep) < 1; @@ -59,15 +49,13 @@ const ContributionWizardFooter: React.FC = ({ Back - - - + {!isLast || !showSubmit ? ( + + + + ) : null} {showSubmit ? ( + + } + > + {name} + {email} + + {editContributorOpen ? ( + { + setName(name); + setEmail(email); + setEditContributorOpen(false); + }} + onClose={() => setEditContributorOpen(false)} + /> + ) : null} + + + + +
+ +