diff --git a/.gitignore b/.gitignore index a7b425f1..3592b7d0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,12 @@ node_modules /public/workspace/* +.eslintrc.json + +.cursorrules + +instructions.md + # local env files *.env* !example.env @@ -38,4 +44,6 @@ yarn-error.log* # Lock files # Вызвало сбой сборки докера # пока вернем до прихода DevOps -# package-lock.json* \ No newline at end of file +# package-lock.json* + +public/build/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4cb95103..78b7499d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,3 @@ -# syntax = docker/dockerfile:1.3 ARG NODE_VERSION=20 @@ -28,6 +27,4 @@ EXPOSE 8080 FROM ghcr.io/rabotaru/dochub/nginx:v0.0.3 as nginx -COPY --chown=101 --from=builder /var/www/dist /usr/share/nginx/html - - +COPY --chown=101 --from=builder /var/www/dist /usr/share/nginx/html \ No newline at end of file diff --git a/instructions.md b/instructions.md new file mode 100644 index 00000000..f61da997 --- /dev/null +++ b/instructions.md @@ -0,0 +1,109 @@ +# Global Search Implementation + +## Overview +Implement a global search functionality that allows users to search across all entities in the system including components, aspects, and documents with content search. + +## Requirements + +### UI Requirements +1. Search Input + - Search input in Menu.vue + - Trigger search on Enter key or search button click + - Clear input button + - Placeholder text "Поиск..." + +2. Search Results Display + - Results shown in main panel using v-data-table + - Columns: + - Type (with color-coded chips: + - "Компонент" (blue) + - "Аспект" (green) + - "Документ" (light blue)) + - Title (clickable link) + - Score (for relevance sorting) + - Content matches shown with: + - "Найдено в содержимом" indicator + - Content snippet with highlighted matches + - Sort results by relevance score + - "Результаты не найдены" message when no matches + +### Search Functionality +1. Components Search + - Search in: + - Component ID + - Component title + - Technologies list + - Display with: + - Blue chip labeled "Компонент" + - Link to component page + - Score: 5 + +2. Aspects Search + - Search in: + - Aspect ID + - Aspect title + - Location + - Display with: + - Green chip labeled "Аспект" + - Link to aspect page + - Score: 5 + +3. Document Content Search + - Search in: + - Document ID + - Document title/description + - Document content + - Document subjects + - Display with: + - Light blue chip labeled "Документ" + - Link to document + - Content snippet + - Score based on match location + +### Technical Implementation +1. Build-time Indexing + - Index documents during build + - Parse markdown content + - Extract headers and keywords + - Store in document-index.json + +2. Runtime Search + - Load document index on startup + - Access Vuex store for components/aspects + - Combine search results + - Sort by relevance score + - Remove duplicates + +3. Search Result Format + ```javascript + { + id: string, // Entity ID + title: string, // Display title + entity: string, // "component" | "aspect" | "document" + link: string, // Router link + score: number, // Relevance score + matchedInContent?: boolean, // For documents only + contentSnippet?: string // For documents only + } + ``` + +## File Structure +- src/frontend/components/Search/SearchResults.vue - Search results component +- src/frontend/manifest/query.js - Search implementation +- src/build/document-indexer.js - Document indexing +- src/build/index-documents.js - Build script + +## Language Requirements +All user interface elements must be in Russian: +1. Search input placeholder: "Поиск..." +2. Entity types: + - Component: "Компонент" + - Aspect: "Аспект" + - Document: "Документ" +3. Search indicators: + - "Найдено в содержимом" for content matches + - "Результаты не найдены" for no results +4. Column headers: + - "Тип" for entity type + - "Заголовок" for title + - "Релевантность" for score diff --git a/package.json b/package.json index 6177222c..d3b31c43 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,13 @@ "backend-up": "node src/backend/main.mjs", "backend": "export VUE_APP_DOCHUB_MODE=backend && npm run build && node src/backend/main.mjs", "serve": "vue-cli-service serve", - "build": "vue-cli-service build", + "build": "npm run build:index && vue-cli-service build", "build:package": "node vue.lib.js", "lint": "vue-cli-service lint", "test": "jest --watch", "meta-import": "node src/tools/meta/import.js", - "postinstall": "find plugins -maxdepth 1 -type d -exec sh -c \"cd '{}' && if [ -f package.json ]; then npm install; fi\" \\;" + "postinstall": "mkdir -p plugins || true && find plugins -maxdepth 1 -type d -exec sh -c \"cd '{}' && if [ -f package.json ]; then npm install; fi\" \\; || true", + "build:index": "node src/build/index-documents.js" }, "dependencies": { "@asyncapi/react-component": "2.3.4", @@ -107,7 +108,9 @@ "vue-template-compiler": "2.7.14", "webpack-bundle-analyzer": "4.8.0", "webpack-pwa-manifest": "4.3.0", - "yo": "4.3.0" + "yo": "4.3.0", + "marked": "^11.1.0", + "keyword-extractor": "^0.0.28" }, "overrides": { "es5-ext": "0.10.53" diff --git a/src/build/document-indexer.js b/src/build/document-indexer.js new file mode 100644 index 00000000..416459dd --- /dev/null +++ b/src/build/document-indexer.js @@ -0,0 +1,177 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const { marked } = require('marked'); +const keywordExtractor = require('keyword-extractor'); + +class DocumentIndexer { + constructor(baseDir = 'public/documentation') { + this.baseDir = baseDir; + this.docTypes = { + markdown: this.processMarkdown, + OpenAPI: this.processOpenAPI, + plantuml: this.processPlantuml + }; + + // Configure marked for header extraction + this.headers = []; + marked.use({ + headerIds: false, + walkTokens: token => { + if (token.type === 'heading') { + this.headers.push({ + level: token.depth, + text: token.text + }); + } + } + }); + } + + // Main indexing function + async indexDocuments() { + const index = {}; + const yamlFiles = this.findYamlFiles(this.baseDir); + + for (const yamlFile of yamlFiles) { + try { + const yamlContent = yaml.load(fs.readFileSync(yamlFile, 'utf8')); + if (yamlContent.docs) { + const docsIndex = await this.processDocsSection(yamlContent.docs, path.dirname(yamlFile)); + Object.assign(index, docsIndex); + } + } catch (error) { + console.error(`Error processing ${yamlFile}:`, error); + } + } + + return index; + } + + // Find all YAML files recursively + findYamlFiles(dir) { + const files = []; + const items = fs.readdirSync(dir); + + for (const item of items) { + const fullPath = path.join(dir, item); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + files.push(...this.findYamlFiles(fullPath)); + } else if (item.endsWith('.yaml') || item.endsWith('.yml')) { + files.push(fullPath); + } + } + + return files; + } + + // Process docs section from YAML + async processDocsSection(docs, yamlDir) { + const index = {}; + + for (const [docId, doc] of Object.entries(docs)) { + if (!doc.source || !doc.type) continue; + + const processor = this.docTypes[doc.type]; + if (!processor) { + console.warn(`Unknown document type: ${doc.type} for ${docId}`); + continue; + } + + try { + const sourcePath = path.join(yamlDir, doc.source); + const { content, headers, keywords } = await processor.call(this, sourcePath); + + index[docId] = { + metadata: { + id: docId, + location: doc.location, + description: doc.description, + type: doc.type, + subjects: doc.subjects || [] + }, + content, + headers, + keywords, + sourcePath: path.relative(this.baseDir, sourcePath) + }; + } catch (error) { + console.error(`Error processing ${docId}:`, error); + } + } + + return index; + } + + // Extract keywords from text + extractKeywords(text) { + return keywordExtractor.extract(text, { + language: "russian", + remove_digits: true, + return_changed_case: true, + remove_duplicates: true + }); + } + + // Document type processors + async processMarkdown(filePath) { + const content = await fs.promises.readFile(filePath, 'utf8'); + + // Reset headers array before parsing + this.headers = []; + + // Parse markdown and extract headers + const parsedContent = marked.parse(content); + const headers = [...this.headers]; // Copy headers array + + // Extract keywords from raw content + const keywords = this.extractKeywords(content); + + return { + content: content, // Store original markdown + parsedContent, // Store HTML version + headers, + keywords + }; + } + + async processOpenAPI(filePath) { + const content = await fs.promises.readFile(filePath, 'utf8'); + const parsedContent = yaml.load(content); + + // Extract keywords from title, description and operation summaries + const keywords = this.extractKeywords([ + parsedContent.info?.title, + parsedContent.info?.description, + ...Object.values(parsedContent.paths || {}) + .flatMap(path => Object.values(path) + .map(op => op.summary)) + ].filter(Boolean).join(' ')); + + return { + content, + parsedContent, + headers: [], + keywords + }; + } + + async processPlantuml(filePath) { + const content = await fs.promises.readFile(filePath, 'utf8'); + + // Extract keywords from PlantUML comments and labels + const keywords = this.extractKeywords( + content.replace(/[@{}]/g, ' ') + ); + + return { + content, + headers: [], + keywords + }; + } +} + +module.exports = DocumentIndexer; \ No newline at end of file diff --git a/src/build/index-documents.js b/src/build/index-documents.js new file mode 100644 index 00000000..68834cae --- /dev/null +++ b/src/build/index-documents.js @@ -0,0 +1,28 @@ +const DocumentIndexer = require('./document-indexer'); +const fs = require('fs'); +const path = require('path'); + +async function buildDocumentIndex() { + try { + const indexer = new DocumentIndexer(); + const index = await indexer.indexDocuments(); + + // Create build directory if it doesn't exist + const buildDir = path.join(__dirname, '../../public/build'); + if (!fs.existsSync(buildDir)) { + fs.mkdirSync(buildDir, { recursive: true }); + } + + // Write index to file + const indexPath = path.join(buildDir, 'document-index.json'); + fs.writeFileSync(indexPath, JSON.stringify(index, null, 2)); + + console.log(`Document index built successfully: ${indexPath}`); + console.log(`Total documents indexed: ${Object.keys(index).length}`); + } catch (error) { + console.error('Error building document index:', error); + process.exit(1); + } +} + +buildDocumentIndex(); \ No newline at end of file diff --git a/src/frontend/components/Layouts/Menu.vue b/src/frontend/components/Layouts/Menu.vue index 7c10d4db..8a2b322f 100644 --- a/src/frontend/components/Layouts/Menu.vue +++ b/src/frontend/components/Layouts/Menu.vue @@ -1,9 +1,18 @@