From 8d644f9dd149c47c54543ee8a92ae9b1ffbcf3c0 Mon Sep 17 00:00:00 2001 From: Brent Salisbury Date: Wed, 18 Dec 2024 01:26:57 -0500 Subject: [PATCH] Add knowledge form context selection - Adds routes for retrieving files from the local knowledge git - Adds the ability to view the file changes/additions in the native dashboard. - Enables the user to view the knowledge document, highlight the text they want to add to the context field and submit it. The highlighted text will get populated to the context field. - Allows the user to still manually enter context or knowledge file details and handles the state switching when they choose to do so. - Prevents the user from selecting context from a different commit SHA if they have already selected context from another SHA. Signed-off-by: Brent Salisbury --- src/app/api/native/git/branches/route.ts | 80 +++- .../api/native/git/knowledge-files/route.ts | 211 ++++++++ src/app/api/native/upload/route.ts | 46 +- .../DocumentInformation.tsx | 188 +++++--- .../KnowledgeQuestionAnswerPairs.tsx | 451 ++++++++++++++++++ .../KnowledgeSeedExampleNative.tsx | 87 ++++ .../Contribute/Knowledge/Native/index.tsx | 325 +++++++++---- .../Knowledge/ReviewSubmission/index.tsx | 48 +- src/components/Dashboard/Native/dashboard.tsx | 90 +++- 9 files changed, 1254 insertions(+), 272 deletions(-) create mode 100644 src/app/api/native/git/knowledge-files/route.ts create mode 100644 src/components/Contribute/Knowledge/Native/KnowledgeQuestionAnswerPairsNative/KnowledgeQuestionAnswerPairs.tsx create mode 100644 src/components/Contribute/Knowledge/Native/KnowledgeSeedExampleNative/KnowledgeSeedExampleNative.tsx diff --git a/src/app/api/native/git/branches/route.ts b/src/app/api/native/git/branches/route.ts index e7942c5e1..716d8cffd 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 + branchDetails.sort((a, b) => b.creationDate - a.creationDate); + 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,18 @@ async function handleDiff(branchName: string, localTaxonomyDir: string) { } const changes = await findDiff(branchName, localTaxonomyDir); - return NextResponse.json({ changes }, { status: 200 }); + // For each added/modified file, read the content from the branch and include it + 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 +167,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,20 +324,34 @@ 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); + } + + // Checkout the file at the given branchName without altering the working directory permanently + // Instead, we use git's internal APIs to read the file from the commit + const branchCommit = await git.resolveRef({ fs, dir: localTaxonomyDir, ref: branchName }); + const { blob } = await git.readBlob({ fs, dir: localTaxonomyDir, oid: branchCommit, filepath: filePath }); + + // Use TextDecoder to properly decode the Uint8Array + 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') { fileMap[fullPath] = entry.oid; } else if (entry.type === 'tree') { - await walkTree(fullPath); // Recursively walk subdirectories + await walkTree(fullPath); } } } 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 000000000..6e4cb8df2 --- /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') { + // Existing behavior: fetch files from 'main' branch + const branchNameForDiff = 'main'; + const knowledgeFiles = await getKnowledgeFiles(branchNameForDiff); + return NextResponse.json({ files: knowledgeFiles }, { status: 200 }); + } + + if (branchName && typeof branchName === 'string') { + // New behavior: fetch files from 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 7473e4f27..b67a452f0 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 3a854a4b6..6f2a71ae1 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; @@ -115,12 +128,12 @@ const DocumentInformation: React.FC = ({ const handleDocumentUpload = async () => { if (uploadedFiles.length > 0) { - const alertInfo: AlertInfo = { + const infoAlert: AlertInfo = { type: 'info', title: 'Document upload(s) in progress!', message: 'Document upload(s) is in progress. You will be notified once the upload successfully completes.' }; - setAlertInfo(alertInfo); + setAlertInfo(infoAlert); const fileContents: { fileName: string; fileContent: string }[] = []; @@ -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 000000000..453012628 --- /dev/null +++ b/src/components/Contribute/Knowledge/Native/KnowledgeQuestionAnswerPairsNative/KnowledgeQuestionAnswerPairs.tsx @@ -0,0 +1,451 @@ +// 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
+  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) => {
+    // Updated function name
+    const selection = window.getSelection();
+    const preElement = preRefs.current[filename];
+    if (selection && preElement) {
+      const anchorNode = selection.anchorNode;
+      const focusNode = selection.focusNode;
+
+      // Check if the selection is within the 
 element
+      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); // Update word count
+      } 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]);
+
+  // Toggle file content expansion
+  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"> + + + +