Skip to content

Commit 5eb64a3

Browse files
authored
Merge pull request #455 from nerdalert/file-conversion-service
Add a docling service file conversion feature
2 parents 5faa13a + 12b84d3 commit 5eb64a3

File tree

6 files changed

+310
-78
lines changed

6 files changed

+310
-78
lines changed

.env.native.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ IL_ENABLE_DEV_MODE=true #Enable this option if you want to enable UI features th
1010
NEXT_PUBLIC_TAXONOMY_ROOT_DIR=
1111

1212
NEXT_PUBLIC_EXPERIMENTAL_FEATURES=false
13+
14+
# IL_FILE_CONVERSION_SERVICE=http://localhost:8000 # Uncomment and fill in the http://host:port if the docling conversion service is running.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// src/app/api/native/convert/route.ts
2+
import { NextResponse } from 'next/server';
3+
4+
`use server`;
5+
6+
interface ConvertRequestBody {
7+
options?: {
8+
output_markdown?: boolean;
9+
include_images?: boolean;
10+
};
11+
file_source: {
12+
base64_string: string;
13+
filename: string;
14+
};
15+
}
16+
17+
// This route calls the external REST service to convert any doc => markdown
18+
export async function POST(request: Request) {
19+
try {
20+
// 1. Parse JSON body from client
21+
const body: ConvertRequestBody = await request.json();
22+
23+
// 2. Read the IL_FILE_CONVERSION_SERVICE from .env (fallback to localhost if not set)
24+
const baseUrl = process.env.IL_FILE_CONVERSION_SERVICE || 'http://localhost:8000';
25+
26+
// 3. Check the health of the conversion service before proceeding
27+
const healthRes = await fetch(`${baseUrl}/health`);
28+
if (!healthRes.ok) {
29+
console.error('The file conversion service is offline or returned non-OK status:', healthRes.status, healthRes.statusText);
30+
return NextResponse.json({ error: 'Conversion service is offline, only markdown files accepted.' }, { status: 503 });
31+
}
32+
33+
// Parse the health response body in case we need to verify its "status":"ok"
34+
const healthData = await healthRes.json();
35+
if (!healthData.status || healthData.status !== 'ok') {
36+
console.error('Doc->md conversion service health check response not "ok":', healthData);
37+
return NextResponse.json({ error: 'Conversion service is offline, only markdown files accepted.' }, { status: 503 });
38+
}
39+
40+
// 4. Service is healthy, proceed with md conversion
41+
const res = await fetch(`${baseUrl}/convert/markdown`, {
42+
method: 'POST',
43+
headers: {
44+
'Content-Type': 'application/json'
45+
},
46+
body: JSON.stringify(body)
47+
});
48+
49+
if (!res.ok) {
50+
console.error('Conversion service responded with error', res.status, res.statusText);
51+
return NextResponse.json({ error: `Conversion service call failed. ${res.statusText}` }, { status: 500 });
52+
}
53+
54+
// 5. Wait for the docling service to return the user submitted file converted to markdown
55+
const data = await res.text();
56+
57+
// Return the markdown wrapped in JSON so the client side can parse it
58+
return NextResponse.json({ content: data }, { status: 200 });
59+
} catch (error: unknown) {
60+
if (error instanceof Error) {
61+
console.error('Error during doc->md conversion route call:', error);
62+
return NextResponse.json({ error: 'md conversion failed.', message: error.message }, { status: 500 });
63+
} else {
64+
console.error('Unknown error during conversion route call:', error);
65+
return NextResponse.json({ error: 'conversion failed due to an unknown error.' }, { status: 500 });
66+
}
67+
}
68+
}

src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
11
// src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx
22
import React, { useEffect, useState } from 'react';
3+
import { FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form';
4+
import { Button } from '@patternfly/react-core/dist/dynamic/components/Button';
5+
import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput';
6+
import { Alert, AlertActionLink, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert';
7+
import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText';
8+
import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText';
9+
import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon';
10+
import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants';
11+
import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal/Modal';
312
import { UploadFile } from '@/components/Contribute/Knowledge/UploadFile';
413
import { checkKnowledgeFormCompletion } from '@/components/Contribute/Knowledge/validation';
514
import { KnowledgeFormData } from '@/types';
6-
import {
7-
ValidatedOptions,
8-
FormFieldGroupHeader,
9-
FormGroup,
10-
Button,
11-
Modal,
12-
ModalVariant,
13-
TextInput,
14-
FormHelperText,
15-
HelperText,
16-
HelperTextItem,
17-
Alert,
18-
AlertActionCloseButton,
19-
AlertActionLink
20-
} from '@patternfly/react-core';
21-
import { ExclamationCircleIcon } from '@patternfly/react-icons';
2215

2316
interface Props {
2417
reset: boolean;
@@ -252,24 +245,29 @@ const DocumentInformation: React.FC<Props> = ({
252245
</Button>
253246
</div>
254247
</FormGroup>
255-
<Modal variant={ModalVariant.medium} title="Data Loss Warning" isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
256-
<p>{modalText}</p>
257-
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '20px' }}>
258-
<Button variant="secondary" onClick={handleModalContinue}>
248+
<Modal
249+
variant={ModalVariant.medium}
250+
title="Data Loss Warning"
251+
titleIconVariant="warning"
252+
isOpen={isModalOpen}
253+
onClose={() => setIsModalOpen(false)}
254+
actions={[
255+
<Button key="Continue" variant="secondary" onClick={handleModalContinue}>
259256
Continue
260-
</Button>
261-
<Button variant="secondary" onClick={() => setIsModalOpen(false)}>
257+
</Button>,
258+
<Button key="cancel" variant="secondary" onClick={() => setIsModalOpen(false)}>
262259
Cancel
263260
</Button>
264-
</div>
261+
]}
262+
>
263+
<p>{modalText}</p>
265264
</Modal>
266265
{!useFileUpload ? (
267266
<>
268267
<FormGroup isRequired key={'doc-info-details-id'} label="Repo URL or Server Side File Path">
269268
<TextInput
270269
isRequired
271-
// TODO: once all of the different potential filepaths/url/types are determined, add back stricter validation
272-
type="text"
270+
type="url"
273271
aria-label="repo"
274272
validated={validRepo}
275273
placeholder="Enter repo URL where document exists"
@@ -328,8 +326,6 @@ const DocumentInformation: React.FC<Props> = ({
328326
</Button>
329327
</>
330328
)}
331-
332-
{/* Informational Alert */}
333329
{alertInfo && (
334330
<Alert variant={alertInfo.type} title={alertInfo.title} actionClose={<AlertActionCloseButton onClose={() => setAlertInfo(undefined)} />}>
335331
{alertInfo.message}
@@ -340,8 +336,6 @@ const DocumentInformation: React.FC<Props> = ({
340336
)}
341337
</Alert>
342338
)}
343-
344-
{/* Success Alert */}
345339
{successAlertTitle && successAlertMessage && (
346340
<Alert
347341
variant="success"
@@ -358,8 +352,6 @@ const DocumentInformation: React.FC<Props> = ({
358352
{successAlertMessage}
359353
</Alert>
360354
)}
361-
362-
{/* Failure Alert */}
363355
{failureAlertTitle && failureAlertMessage && (
364356
<Alert variant="danger" title={failureAlertTitle} actionClose={<AlertActionCloseButton onClose={onCloseFailureAlert} />}>
365357
{failureAlertMessage}

src/components/Contribute/Knowledge/Native/KnowledgeQuestionAnswerPairsNative/KnowledgeQuestionAnswerPairs.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ const KnowledgeQuestionAnswerPairsNative: React.FC<Props> = ({
6060
const [expandedFiles, setExpandedFiles] = useState<Record<string, boolean>>({});
6161
const [selectedWordCount, setSelectedWordCount] = useState<number>(0);
6262
const [showAllCommits, setShowAllCommits] = useState<boolean>(false);
63+
const [contextWordCount, setContextWordCount] = useState(0);
64+
const MAX_WORDS = 500;
6365

6466
// Ref for the <pre> elements to track selections TODO: figure out how to make text expansions taller in PF without a custom-pre
6567
const preRefs = useRef<Record<string, HTMLPreElement | null>>({});
@@ -217,6 +219,27 @@ const KnowledgeQuestionAnswerPairsNative: React.FC<Props> = ({
217219
[]
218220
);
219221

222+
// TODO: replace with a tokenizer library
223+
const countWords = (text: string) => {
224+
return text.trim().split(/\s+/).filter(Boolean).length;
225+
};
226+
227+
// Update word count whenever context changes
228+
useEffect(() => {
229+
setContextWordCount(countWords(seedExample.context));
230+
}, [seedExample.context]);
231+
232+
// Handle context input change with word count validation
233+
const onContextChange = (_event: React.FormEvent<HTMLTextAreaElement>, contextValue: string) => {
234+
const wordCount = countWords(contextValue);
235+
if (wordCount <= MAX_WORDS) {
236+
handleContextInputChange(seedExampleIndex, contextValue);
237+
} else {
238+
// allow the overage and show validation error
239+
handleContextInputChange(seedExampleIndex, contextValue);
240+
}
241+
};
242+
220243
return (
221244
<FormGroup style={{ padding: '20px' }}>
222245
<Tooltip content={<div>Select context from your knowledge files</div>} position="top">
@@ -232,11 +255,18 @@ const KnowledgeQuestionAnswerPairsNative: React.FC<Props> = ({
232255
placeholder="Enter the context from which the Q&A pairs are derived. (500 words max)"
233256
value={seedExample.context}
234257
validated={seedExample.isContextValid}
235-
maxLength={500}
258+
onChange={onContextChange}
236259
style={{ marginBottom: '20px' }}
237-
onChange={(_event, contextValue: string) => handleContextInputChange(seedExampleIndex, contextValue)}
238260
onBlur={() => handleContextBlur(seedExampleIndex)}
239261
/>
262+
{/* Display word count */}
263+
<FormHelperText>
264+
<HelperText>
265+
<HelperTextItem>
266+
{contextWordCount} / {MAX_WORDS} words
267+
</HelperTextItem>
268+
</HelperText>
269+
</FormHelperText>
240270
{seedExample.isContextValid === ValidatedOptions.error && (
241271
<FormHelperText>
242272
<HelperText>
@@ -252,7 +282,6 @@ const KnowledgeQuestionAnswerPairsNative: React.FC<Props> = ({
252282
<div
253283
style={{
254284
padding: '10px',
255-
// backgroundColor: '#f2f2f2',
256285
borderRadius: '5px',
257286
marginBottom: '10px',
258287
fontSize: '14px',

src/components/Contribute/Knowledge/Native/index.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -490,14 +490,16 @@ export const KnowledgeFormNative: React.FunctionComponent<KnowledgeFormProps> =
490490
immutable: true,
491491
isExpanded: false,
492492
context: yamlSeedExample.context,
493-
// TODO: Hardcoding yaml QnA uploads seed_examples to success until working with Validate.tsx - Bug #465
494-
isContextValid: ValidatedOptions.success,
493+
isContextValid: ValidatedOptions.default,
494+
validationError: '',
495495
questionAndAnswers: yamlSeedExample.questions_and_answers.map((qa) => ({
496496
immutable: true,
497497
question: qa.question,
498498
answer: qa.answer,
499-
isQuestionValid: ValidatedOptions.success,
500-
isAnswerValid: ValidatedOptions.success
499+
isQuestionValid: ValidatedOptions.default,
500+
questionValidationError: '',
501+
isAnswerValid: ValidatedOptions.default,
502+
answerValidationError: ''
501503
}))
502504
}));
503505

0 commit comments

Comments
 (0)