diff --git a/src/app/api/native/git/branches/route.ts b/src/app/api/native/git/branches/route.ts index e7942c5e..c6e0e68f 100644 --- a/src/app/api/native/git/branches/route.ts +++ b/src/app/api/native/git/branches/route.ts @@ -12,6 +12,7 @@ const REMOTE_TAXONOMY_REPO_CONTAINER_MOUNT_DIR = '/tmp/.instructlab-ui'; interface Diffs { file: string; status: string; + content?: string; } export async function GET() { @@ -38,15 +39,15 @@ export async function GET() { const messageStr = commitMessage.split('Signed-off-by'); branchDetails.push({ name: branch, - creationDate: commitDetails.commit.committer.timestamp * 1000, // Convert to milliseconds + creationDate: commitDetails.commit.committer.timestamp * 1000, message: messageStr[0].replace(/\n+$/, ''), author: signoff }); } branchDetails.sort((a, b) => b.creationDate - a.creationDate); // Sort by creation date, newest first + console.log('Total branches present in native taxonomy:', branchDetails.length); - console.log('Total branches present in local taxonomy:', branchDetails.length); return NextResponse.json({ branches: branchDetails }, { status: 200 }); } catch (error) { console.error('Failed to list branches from local taxonomy:', error); @@ -131,7 +132,17 @@ async function handleDiff(branchName: string, localTaxonomyDir: string) { } const changes = await findDiff(branchName, localTaxonomyDir); - return NextResponse.json({ changes }, { status: 200 }); + const enrichedChanges: Diffs[] = []; + for (const change of changes) { + if (change.status === 'added' || change.status === 'modified') { + const fileContent = await readFileFromBranch(localTaxonomyDir, branchName, change.file); + enrichedChanges.push({ ...change, content: fileContent }); + } else { + enrichedChanges.push(change); + } + } + + return NextResponse.json({ changes: enrichedChanges }, { status: 200 }); } catch (error) { console.error(`Failed to show contribution changes ${branchName}:`, error); return NextResponse.json( @@ -155,8 +166,8 @@ async function findDiff(branchName: string, localTaxonomyDir: string): Promise 0) { - const remoteBranchName = branchName; await git.checkout({ fs, dir: localTaxonomyDir, ref: branchName }); // Read the commit message of the top commit from the branch const details = await getTopCommitDetails(localTaxonomyDir); // Check if the remote branch exists, if not create it + const remoteBranchName = branchName; const remoteBranchExists = await git.listBranches({ fs, dir: remoteTaxonomyDir }); if (remoteBranchExists.includes(remoteBranchName)) { - console.log(`Branch ${remoteBranchName} exist in remote taxonomy, deleting it.`); - // Delete the remote branch if it exists, we will recreate it + console.log(`Branch ${remoteBranchName} exists in remote taxonomy, deleting it.`); await git.deleteBranch({ fs, dir: remoteTaxonomyDir, ref: remoteBranchName }); } else { console.log(`Branch ${remoteBranchName} does not exist in remote taxonomy, creating a new branch.`); @@ -243,14 +254,21 @@ async function handlePublish(branchName: string, localTaxonomyDir: string, remot // Copy the files listed in the changes array to the remote branch and if the directories do not exist, create them for (const change of changes) { - console.log(`Copying ${change.file} to remote branch ${remoteBranchName}`); - const filePath = path.join(localTaxonomyDir, change.file); - const remoteFilePath = path.join(remoteTaxonomyDir, change.file); - const remoteFileDir = path.dirname(remoteFilePath); - if (!fs.existsSync(remoteFileDir)) { - fs.mkdirSync(remoteFileDir, { recursive: true }); + if (change.status !== 'deleted') { + const filePath = path.join(localTaxonomyDir, change.file); + const remoteFilePath = path.join(remoteTaxonomyDir, change.file); + const remoteFileDir = path.dirname(remoteFilePath); + if (!fs.existsSync(remoteFileDir)) { + fs.mkdirSync(remoteFileDir, { recursive: true }); + } + fs.copyFileSync(filePath, remoteFilePath); + } else { + // If deleted, ensure the file is removed from remote as well, if it exists + const remoteFilePath = path.join(remoteTaxonomyDir, change.file); + if (fs.existsSync(remoteFilePath)) { + fs.rmSync(remoteFilePath); + } } - fs.copyFileSync(filePath, remoteFilePath); } await git.add({ fs, dir: remoteTaxonomyDir, filepath: '.' }); @@ -306,14 +324,25 @@ async function handlePublish(branchName: string, localTaxonomyDir: string, remot } } -// Helper function to recursively gather file paths and their oids from a tree -async function getFilesFromTree(commitOid: string) { - const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); +async function readFileFromBranch(localTaxonomyDir: string, branchName: string, filePath: string): Promise { + const tempDir = path.join(localTaxonomyDir, '.temp_checkout'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir); + } + + const branchCommit = await git.resolveRef({ fs, dir: localTaxonomyDir, ref: branchName }); + const { blob } = await git.readBlob({ fs, dir: localTaxonomyDir, oid: branchCommit, filepath: filePath }); + + const decoder = new TextDecoder('utf-8'); + const content = decoder.decode(blob); + return content; +} + +async function getFilesFromTree(commitOid: string, repoDir: string) { const fileMap: Record = {}; async function walkTree(dir: string) { - const tree = await git.readTree({ fs, dir: REPO_DIR, oid: commitOid, filepath: dir }); - + const tree = await git.readTree({ fs, dir: repoDir, oid: commitOid, filepath: dir }); for (const entry of tree.tree) { const fullPath = path.join(dir, entry.path); if (entry.type === 'blob') { diff --git a/src/app/api/native/git/knowledge-files/route.ts b/src/app/api/native/git/knowledge-files/route.ts new file mode 100644 index 00000000..1d6a85bc --- /dev/null +++ b/src/app/api/native/git/knowledge-files/route.ts @@ -0,0 +1,211 @@ +// src/app/api/native/git/knowledge-files/route.ts + +import { NextRequest, NextResponse } from 'next/server'; +import * as git from 'isomorphic-git'; +import fs from 'fs'; +import path from 'path'; + +// Constants for repository paths +const LOCAL_TAXONOMY_DOCS_ROOT_DIR = + process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_DOCS_ROOT_DIR || `${process.env.HOME}/.instructlab-ui/taxonomy-knowledge-docs`; + +// Interface for the response +interface KnowledgeFile { + filename: string; + content: string; + commitSha: string; + commitDate: string; +} + +interface Branch { + name: string; + commitSha: string; + commitDate: string; +} + +/** + * Function to list all branches. + */ +const listAllBranches = async (): Promise => { + const REPO_DIR = LOCAL_TAXONOMY_DOCS_ROOT_DIR; + + if (!fs.existsSync(REPO_DIR)) { + throw new Error('Repository path does not exist.'); + } + + const branches = await git.listBranches({ fs, dir: REPO_DIR }); + + const branchDetails: Branch[] = []; + + for (const branch of branches) { + try { + const latestCommit = await git.log({ fs, dir: REPO_DIR, ref: branch, depth: 1 }); + if (latestCommit.length === 0) { + continue; // No commits on this branch + } + + const commit = latestCommit[0]; + const commitSha = commit.oid; + const commitDate = new Date(commit.commit.committer.timestamp * 1000).toISOString(); + + branchDetails.push({ + name: branch, + commitSha: commitSha, + commitDate: commitDate + }); + } catch (error) { + console.error(`Failed to retrieve commit for branch ${branch}:`, error); + continue; + } + } + + return branchDetails; +}; + +/** + * Function to retrieve knowledge files from a specific branch. + * @param branchName - The name of the branch to retrieve files from. + * @returns An array of KnowledgeFile objects. + */ +const getKnowledgeFiles = async (branchName: string): Promise => { + const REPO_DIR = LOCAL_TAXONOMY_DOCS_ROOT_DIR; + + // Ensure the repository path exists + if (!fs.existsSync(REPO_DIR)) { + throw new Error('Repository path does not exist.'); + } + + // Check if the branch exists + const branches = await git.listBranches({ fs, dir: REPO_DIR }); + if (!branches.includes(branchName)) { + throw new Error(`Branch "${branchName}" does not exist.`); + } + + // Checkout the specified branch + await git.checkout({ fs, dir: REPO_DIR, ref: branchName }); + + // 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: KnowledgeFile[] = []; + + 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: branchName, + 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 = new Date(latestCommit.commit.committer.timestamp * 1000).toISOString(); + + // Read the file content + const fileContent = fs.readFileSync(filePath, 'utf-8'); + + knowledgeFiles.push({ + filename: file, + content: fileContent, + commitSha: commitSha, + commitDate: commitDate + }); + } catch (error) { + console.error(`Failed to retrieve commit for file ${file}:`, error); + // Skip files that cause errors + continue; + } + } + + return knowledgeFiles; +}; + +/** + * Handler for GET requests. + * - If 'action=list-branches' is present, return all branches. + * - Else, return knowledge files from the 'main' branch. + */ +const getKnowledgeFilesHandler = async (req: NextRequest): Promise => { + try { + const { searchParams } = new URL(req.url); + const action = searchParams.get('action'); + + if (action === 'list-branches') { + const branches = await listAllBranches(); + return NextResponse.json({ branches }, { status: 200 }); + } + + // Default behavior: fetch files from 'main' branch + const branchName = 'main'; + const knowledgeFiles = await getKnowledgeFiles(branchName); + return NextResponse.json({ files: knowledgeFiles }, { status: 200 }); + } catch (error) { + console.error('Failed to retrieve knowledge files:', error); + return NextResponse.json({ error: (error as Error).message }, { status: 500 }); + } +}; + +/** + * Handler for POST requests. + * - If 'branchName' is provided, fetch files for that branch. + * - If 'action=diff', fetch files from the 'main' branch. + * - Else, return an error. + */ +const postKnowledgeFilesHandler = async (req: NextRequest): Promise => { + try { + const body = await req.json(); + const { action, branchName } = body; + + if (action === 'diff') { + // fetch files from main + const branchNameForDiff = 'main'; + const knowledgeFiles = await getKnowledgeFiles(branchNameForDiff); + return NextResponse.json({ files: knowledgeFiles }, { status: 200 }); + } + + if (branchName && typeof branchName === 'string') { + // Fetch files from a specified branch + const knowledgeFiles = await getKnowledgeFiles(branchName); + return NextResponse.json({ files: knowledgeFiles }, { status: 200 }); + } + + // If no valid action or branchName is provided + return NextResponse.json({ error: 'Invalid request. Provide an action or branchName.' }, { status: 400 }); + } catch (error) { + console.error('Failed to process POST request:', error); + return NextResponse.json({ error: (error as Error).message }, { status: 500 }); + } +}; + +/** + * GET handler to retrieve knowledge files or list branches based on 'action' query parameter. + */ +export async function GET(req: NextRequest) { + return await getKnowledgeFilesHandler(req); +} + +/** + * POST handler to retrieve knowledge files based on 'branchName' or 'action'. + */ +export async function POST(req: NextRequest) { + return await postKnowledgeFilesHandler(req); +} diff --git a/src/app/api/native/upload/route.ts b/src/app/api/native/upload/route.ts index 7473e4f2..b67a452f 100644 --- a/src/app/api/native/upload/route.ts +++ b/src/app/api/native/upload/route.ts @@ -6,8 +6,7 @@ import http from 'isomorphic-git/http/node'; import path from 'path'; import fs from 'fs'; -const TAXONOMY_DOCS_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || ''; -const TAXONOMY_DOCS_CONTAINER_MOUNT_DIR = '/tmp/.instructlab-ui'; +const LOCAL_TAXONOMY_DOCS_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; const TAXONOMY_KNOWLEDGE_DOCS_REPO_URL = 'https://github.com/instructlab-public/taxonomy-knowledge-docs.git'; export async function POST(req: NextRequest) { @@ -31,10 +30,8 @@ export async function POST(req: NextRequest) { }); // Write the files to the repository - const docsRepoUrlTmp = path.join(docsRepoUrl, '/'); for (const file of filesWithTimestamp) { - const filePath = path.join(docsRepoUrlTmp, file.fileName); - console.log(`Writing file to ${filePath} in taxonomy knowledge docs repository.`); + const filePath = path.join(docsRepoUrl, file.fileName); fs.writeFileSync(filePath, file.fileContent); } @@ -54,12 +51,9 @@ export async function POST(req: NextRequest) { .join(', ')}\n\nSigned-off-by: ui@instructlab.ai` }); - console.log(`Successfully committed files to taxonomy knowledge docs repository with commit SHA: ${commitSha}`); - - const origTaxonomyDocsRepoDir = path.join(TAXONOMY_DOCS_ROOT_DIR, '/taxonomy-knowledge-docs'); return NextResponse.json( { - repoUrl: origTaxonomyDocsRepoDir, + repoUrl: docsRepoUrl, commitSha, documentNames: filesWithTimestamp.map((file: { fileName: string }) => file.fileName), prUrl: '' @@ -67,32 +61,17 @@ export async function POST(req: NextRequest) { { status: 201 } ); } catch (error) { - console.error('Failed to upload knowledge documents:', error); - return NextResponse.json({ error: 'Failed to upload knowledge documents' }, { status: 500 }); + console.error('Failed to upload documents:', error); + return NextResponse.json({ error: 'Failed to upload documents' }, { status: 500 }); } } async function cloneTaxonomyDocsRepo() { - // Check the location of the taxonomy repository and create the taxonomy-docs-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'); + const taxonomyDocsDirectoryPath = path.join(LOCAL_TAXONOMY_DOCS_ROOT_DIR, '/taxonomy-knowledge-docs'); + console.log(`Cloning taxonomy docs repository to ${taxonomyDocsDirectoryPath}...`); if (fs.existsSync(taxonomyDocsDirectoryPath)) { - console.log(`Using existing taxonomy knowledge docs repository at ${remoteTaxonomyRepoDir}/taxonomy-knowledge-docs.`); + console.log(`Using existing taxonomy knowledge docs repository at ${taxonomyDocsDirectoryPath}.`); return taxonomyDocsDirectoryPath; } else { console.log(`Taxonomy knowledge docs repository not found at ${taxonomyDocsDirectoryPath}. Cloning...`); @@ -104,13 +83,12 @@ async function cloneTaxonomyDocsRepo() { http, dir: taxonomyDocsDirectoryPath, url: TAXONOMY_KNOWLEDGE_DOCS_REPO_URL, - singleBranch: true + singleBranch: true, + depth: 1 }); - // 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) + // Include the full path in the response for client display + console.log(`Repository cloned successfully to ${taxonomyDocsDirectoryPath}.`); return taxonomyDocsDirectoryPath; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; diff --git a/src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx b/src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx index 3a854a4b..3b81185c 100644 --- a/src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx +++ b/src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx @@ -1,8 +1,9 @@ +// src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx import React, { useEffect, useState } from 'react'; import { FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; -import { Alert, AlertActionLink, AlertActionCloseButton, AlertGroup } from '@patternfly/react-core/dist/dynamic/components/Alert'; +import { Alert, AlertActionLink, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert'; import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; @@ -17,14 +18,24 @@ interface Props { isEditForm?: boolean; knowledgeFormData: KnowledgeFormData; setDisableAction: React.Dispatch>; + knowledgeDocumentRepositoryUrl: string; setKnowledgeDocumentRepositoryUrl: React.Dispatch>; + knowledgeDocumentCommit: string; setKnowledgeDocumentCommit: React.Dispatch>; + documentName: string; setDocumentName: React.Dispatch>; } +interface AlertInfo { + type: 'success' | 'danger' | 'info'; + title: string; + message: string; + link?: string; +} + const DocumentInformation: React.FC = ({ reset, isEditForm, @@ -41,17 +52,19 @@ const DocumentInformation: React.FC = ({ const [uploadedFiles, setUploadedFiles] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [modalText, setModalText] = useState(); + + const [successAlertTitle, setSuccessAlertTitle] = useState(); + const [successAlertMessage, setSuccessAlertMessage] = useState(); + const [successAlertLink, setSuccessAlertLink] = useState(); + + const [failureAlertTitle, setFailureAlertTitle] = useState(); + const [failureAlertMessage, setFailureAlertMessage] = useState(); const [alertInfo, setAlertInfo] = useState(); - const [validRepo, setValidRepo] = useState(); - const [validCommit, setValidCommit] = useState(); - const [validDocumentName, setValidDocumentName] = useState(); - interface AlertInfo { - type: 'success' | 'danger' | 'info'; - title: string; - message: string; - link?: string; - } + const [validRepo, setValidRepo] = useState(ValidatedOptions.default); + const [validCommit, setValidCommit] = useState(ValidatedOptions.default); + const [validDocumentName, setValidDocumentName] = useState(ValidatedOptions.default); + useEffect(() => { setValidRepo(ValidatedOptions.default); setValidCommit(ValidatedOptions.default); @@ -98,8 +111,8 @@ const DocumentInformation: React.FC = ({ }; const validateDocumentName = (document: string) => { - const documentName = document.trim(); - if (documentName.length > 0) { + const documentNameStr = document.trim(); + if (documentNameStr.length > 0) { setValidDocumentName(ValidatedOptions.success); setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); return; @@ -141,47 +154,49 @@ const DocumentInformation: React.FC = ({ ); if (fileContents.length === uploadedFiles.length) { - const response = await fetch('/api/native/upload', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ files: fileContents }) - }); - - if (!response.ok) { - const alertInfo: AlertInfo = { - type: 'danger', - title: 'Document upload failed!', - message: `Upload failed for the added documents. ${response.statusText}` - }; - setAlertInfo(alertInfo); - new Error(response.statusText || 'Document upload failed'); - return; - } + try { + const response = await fetch('/api/native/upload', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ files: fileContents }) + }); - const result = await response.json(); + if (response.status === 201) { + const result = await response.json(); + console.log('Files uploaded result:', result); - setKnowledgeDocumentRepositoryUrl(result.repoUrl); - setKnowledgeDocumentCommit(result.commitSha); - setDocumentName(result.documentNames.join(', ')); // Populate the patterns field - console.log('Files uploaded:', result.documentNames); - - const alertInfo: AlertInfo = { - type: 'success', - title: 'Document uploaded successfully!', - message: 'Documents have been uploaded to your repo to be referenced in the knowledge submission.' - }; - if (result.prUrl !== '') { - alertInfo.link = result.prUrl; + setSuccessAlertTitle('Document uploaded successfully!'); + setSuccessAlertMessage('Documents have been uploaded to your repo to be referenced in the knowledge submission.'); + if (result.prUrl && result.prUrl.trim() !== '') { + setSuccessAlertLink(result.prUrl); + } else { + setSuccessAlertLink(undefined); + } + } else { + console.error('Upload failed:', response.statusText); + setFailureAlertTitle('Failed to upload document'); + setFailureAlertMessage(`This upload failed. ${response.statusText}`); + } + } catch (error) { + console.error('Upload error:', error); + setFailureAlertTitle('Failed to upload document'); + setFailureAlertMessage(`This upload failed. ${(error as Error).message}`); } - setAlertInfo(alertInfo); } } }; const onCloseSuccessAlert = () => { - setAlertInfo(undefined); + setSuccessAlertTitle(undefined); + setSuccessAlertMessage(undefined); + setSuccessAlertLink(undefined); + }; + + const onCloseFailureAlert = () => { + setFailureAlertTitle(undefined); + setFailureAlertMessage(undefined); }; const handleAutomaticUpload = () => { @@ -206,6 +221,7 @@ const DocumentInformation: React.FC = ({ if (useFileUpload) { setUploadedFiles([]); } else { + console.log('Switching to manual entry - clearing repository and document info'); setKnowledgeDocumentRepositoryUrl(''); setValidRepo(ValidatedOptions.default); setKnowledgeDocumentCommit(''); @@ -219,20 +235,30 @@ const DocumentInformation: React.FC = ({ return (
- + + Document Information * +

+ ), + id: 'doc-info-id' + }} + titleDescription="Add the relevant document's information" + />
@@ -245,7 +271,7 @@ const DocumentInformation: React.FC = ({ isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} actions={[ - , )} + {/* Informational Alert */} {alertInfo && ( - - } - actionLinks={ - alertInfo.link && ( - <> - - View it here - - - ) - } - > - {alertInfo.message} - - + setAlertInfo(undefined)} />}> + {alertInfo.message} + {alertInfo.link && ( + + View it here + + )} + + )} + + {/* Success Alert */} + {successAlertTitle && successAlertMessage && ( + } + actionLinks={ + successAlertLink ? ( + + View it here + + ) : null + } + > + {successAlertMessage} + + )} + + {/* Failure Alert */} + {failureAlertTitle && failureAlertMessage && ( + }> + {failureAlertMessage} + )}
); diff --git a/src/components/Contribute/Knowledge/Native/KnowledgeQuestionAnswerPairsNative/KnowledgeQuestionAnswerPairs.tsx b/src/components/Contribute/Knowledge/Native/KnowledgeQuestionAnswerPairsNative/KnowledgeQuestionAnswerPairs.tsx new file mode 100644 index 00000000..a0f78b8f --- /dev/null +++ b/src/components/Contribute/Knowledge/Native/KnowledgeQuestionAnswerPairsNative/KnowledgeQuestionAnswerPairs.tsx @@ -0,0 +1,448 @@ +// src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { TextArea } from '@patternfly/react-core/dist/dynamic/components/TextArea'; +import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/dynamic/icons/'; +import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; +import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { KnowledgeSeedExample, QuestionAndAnswerPair } from '@/types'; +import { Modal, ModalVariant } from '@patternfly/react-core/dist/dynamic/components/Modal'; +import { Tooltip } from '@patternfly/react-core/dist/esm/components/Tooltip/Tooltip'; +import { CatalogIcon } from '@patternfly/react-icons/dist/esm/icons/catalog-icon'; +import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; +import { Spinner } from '@patternfly/react-core/dist/dynamic/components/Spinner'; +import { ExpandableSection } from '@patternfly/react-core/dist/esm/components/ExpandableSection/ExpandableSection'; +import { Content } from '@patternfly/react-core/dist/dynamic/components/Content'; +import { Switch } from '@patternfly/react-core/dist/dynamic/components/Switch'; +import { Card, CardBody, CardHeader } from '@patternfly/react-core/dist/dynamic/components/Card'; +import { Stack, StackItem } from '@patternfly/react-core/dist/dynamic/layouts/Stack'; +import { Alert } from '@patternfly/react-core/dist/dynamic/components/Alert'; + +interface KnowledgeFile { + filename: string; + content: string; + commitSha: string; + commitDate?: string; +} + +interface Props { + seedExample: KnowledgeSeedExample; + seedExampleIndex: number; + handleContextInputChange: (seedExampleIndex: number, contextValue: string) => void; + handleContextBlur: (seedExampleIndex: number) => void; + handleQuestionInputChange: (seedExampleIndex: number, questionAndAnswerIndex: number, questionValue: string) => void; + handleQuestionBlur: (seedExampleIndex: number, questionAndAnswerIndex: number) => void; + handleAnswerInputChange: (seedExampleIndex: number, questionAndAnswerIndex: number, answerValue: string) => void; + handleAnswerBlur: (seedExampleIndex: number, questionAndAnswerIndex: number) => void; + addDocumentInfo: (repositoryUrl: string, commitSha: string, docName: string) => void; + repositoryUrl: string; + commitSha: string; +} + +const KnowledgeQuestionAnswerPairsNative: React.FC = ({ + seedExample, + seedExampleIndex, + handleContextInputChange, + handleContextBlur, + handleQuestionInputChange, + handleQuestionBlur, + handleAnswerInputChange, + handleAnswerBlur, + addDocumentInfo, + repositoryUrl, + commitSha +}) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [knowledgeFiles, setKnowledgeFiles] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const [expandedFiles, setExpandedFiles] = useState>({}); + const [selectedWordCount, setSelectedWordCount] = useState(0); + const [showAllCommits, setShowAllCommits] = useState(false); + + // Ref for the
 elements to track selections TODO: figure out how to make text expansions taller in PF without a custom-pre
+  const preRefs = useRef>({});
+
+  const LOCAL_TAXONOMY_DOCS_ROOT_DIR =
+    process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_DOCS_ROOT_DIR || '/home/yourusername/.instructlab-ui/taxonomy-knowledge-docs';
+
+  const fetchKnowledgeFiles = async () => {
+    setIsLoading(true);
+    setError('');
+    try {
+      const response = await fetch('/api/native/git/knowledge-files', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ branchName: 'main', action: 'diff' })
+      });
+
+      const result = await response.json();
+      if (response.ok) {
+        setKnowledgeFiles(result.files);
+        console.log('Fetched knowledge files:', result.files);
+      } else {
+        setError(result.error || 'Failed to fetch knowledge files.');
+        console.error('Error fetching knowledge files:', result.error);
+      }
+    } catch (err) {
+      setError('An error occurred while fetching knowledge files.');
+      console.error('Error fetching knowledge files:', err);
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  const handleOpenModal = () => {
+    setIsModalOpen(true);
+    fetchKnowledgeFiles();
+  };
+
+  const handleCloseModal = () => {
+    setIsModalOpen(false);
+    setKnowledgeFiles([]);
+    setError('');
+    setSelectedWordCount(0);
+    setShowAllCommits(false);
+    window.getSelection()?.removeAllRanges();
+  };
+
+  const handleUseSelectedText = (file: KnowledgeFile) => {
+    const selection = window.getSelection();
+    const selectedText = selection?.toString().trim();
+
+    if (!selectedText) {
+      alert('Please select the text you want to use as context.');
+      return;
+    }
+
+    repositoryUrl = `${LOCAL_TAXONOMY_DOCS_ROOT_DIR}/${file.filename}`;
+    const commitShaValue = file.commitSha;
+    const docName = file.filename;
+
+    console.log(
+      `handleUseSelectedText: selectedText="${selectedText}", repositoryUrl=${repositoryUrl}, commitSha=${commitShaValue}, docName=${docName}`
+    );
+
+    handleContextInputChange(seedExampleIndex, selectedText);
+    handleContextBlur(seedExampleIndex);
+    addDocumentInfo(repositoryUrl, commitShaValue, docName);
+    handleCloseModal();
+  };
+
+  const updateSelectedWordCount = (filename: string) => {
+    const selection = window.getSelection();
+    const preElement = preRefs.current[filename];
+    if (selection && preElement) {
+      const anchorNode = selection.anchorNode;
+      const focusNode = selection.focusNode;
+
+      if (preElement.contains(anchorNode) && preElement.contains(focusNode)) {
+        const selectedText = selection.toString().trim();
+        const wordCount = selectedText.split(/\s+/).filter((word) => word.length > 0).length;
+        setSelectedWordCount(wordCount);
+      } else {
+        setSelectedWordCount(0);
+      }
+    }
+  };
+
+  // Attach event listeners for selection changes
+  useEffect(() => {
+    if (isModalOpen) {
+      const handleSelectionChange = () => {
+        // Iterate through all expanded files and update word count
+        Object.keys(expandedFiles).forEach((filename) => {
+          if (expandedFiles[filename]) {
+            updateSelectedWordCount(filename);
+          }
+        });
+      };
+      document.addEventListener('selectionchange', handleSelectionChange);
+      return () => {
+        document.removeEventListener('selectionchange', handleSelectionChange);
+      };
+    } else {
+      setSelectedWordCount(0);
+    }
+  }, [isModalOpen, expandedFiles]);
+
+  const toggleFileContent = (filename: string) => {
+    setExpandedFiles((prev) => ({
+      ...prev,
+      [filename]: !prev[filename]
+    }));
+    console.log(`toggleFileContent: filename=${filename}, expanded=${!expandedFiles[filename]}`);
+  };
+
+  // Group files by commitSha
+  const groupedFiles = knowledgeFiles.reduce>((acc, file) => {
+    if (!acc[file.commitSha]) {
+      acc[file.commitSha] = [];
+    }
+    acc[file.commitSha].push(file);
+    return acc;
+  }, {});
+
+  // Extract commit dates for sorting
+  const commitDateMap: Record = {};
+  knowledgeFiles.forEach((file) => {
+    if (file.commitDate && !commitDateMap[file.commitSha]) {
+      commitDateMap[file.commitSha] = file.commitDate;
+    }
+  });
+
+  // Sort the commit SHAs based on commitDate in descending order (latest first)
+  const sortedCommitShas = Object.keys(groupedFiles).sort((a, b) => {
+    const dateA = new Date(commitDateMap[a] || '').getTime();
+    const dateB = new Date(commitDateMap[b] || '').getTime();
+    return dateB - dateA;
+  });
+
+  // Enforce single commit SHA and repository URL
+  const isSameCommit = (fileCommitSha: string): boolean => {
+    if (!commitSha) {
+      return true;
+    }
+    return fileCommitSha === commitSha;
+  };
+
+  // Determine which commits to display based on the toggle
+  const commitsToDisplay = showAllCommits ? sortedCommitShas : sortedCommitShas.slice(0, 1);
+
+  const setPreRef = useCallback(
+    (filename: string) => (el: HTMLPreElement | null) => {
+      preRefs.current[filename] = el;
+    },
+    []
+  );
+
+  return (
+    
+      Select context from your knowledge files
} position="top"> + + + +