diff --git a/.mimzi/tasks/task-content-processing.yaml b/.mimzi/tasks/task-content-processing.yaml index fc89871..02d1450 100644 --- a/.mimzi/tasks/task-content-processing.yaml +++ b/.mimzi/tasks/task-content-processing.yaml @@ -15,6 +15,7 @@ description: | - Search implementation - Content loading system assigned_to: worker-1 -status: in_progress +status: failed +result: 'qwen: agent loop: agent exceeded max steps (20)' created_at: 2026-04-08T00:01:49.285216405Z -updated_at: 2026-04-08T00:05:44.477892521Z +updated_at: 2026-04-08T00:09:25.144326221Z diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index b5e8111..533d357 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -1,12 +1,21 @@ import React, { useState } from 'react'; import './SearchBar.css'; -const SearchBar = ({ onSearch }) => { +const SearchBar = ({ onSearch, categories }) => { const [searchTerm, setSearchTerm] = useState(''); + const [selectedCategory, setSelectedCategory] = useState('All'); const handleSubmit = (e) => { e.preventDefault(); - onSearch(searchTerm); + onSearch(searchTerm, selectedCategory); + }; + + const handleSearchChange = (e) => { + setSearchTerm(e.target.value); + }; + + const handleCategoryChange = (e) => { + setSelectedCategory(e.target.value); }; return ( @@ -16,8 +25,14 @@ const SearchBar = ({ onSearch }) => { type="text" placeholder="Search jargon terms..." value={searchTerm} - onChange={(e) => setSearchTerm(e.target.value)} + onChange={handleSearchChange} /> + diff --git a/src/models/JargonEntry.js b/src/models/JargonEntry.js new file mode 100644 index 0000000..0036b95 --- /dev/null +++ b/src/models/JargonEntry.js @@ -0,0 +1,172 @@ +/** + * Data model for Jargon File entries + */ + +export class JargonEntry { + constructor(data) { + this.id = data.id || Date.now() + Math.floor(Math.random() * 1000); + this.term = data.term || ''; + this.definition = data.definition || ''; + this.category = data.category || 'Uncategorized'; + this.relatedTerms = data.relatedTerms || []; + this.dateAdded = data.dateAdded || new Date().toISOString().split('T')[0]; + this.dateModified = data.dateModified || new Date().toISOString().split('T')[0]; + } + + /** + * Validates that the entry has required fields + * @returns {boolean} True if valid + */ + isValid() { + return this.term && this.definition; + } + + /** + * Updates the entry with new data + * @param {Object} newData - New data to update + */ + update(newData) { + Object.assign(this, newData); + this.dateModified = new Date().toISOString().split('T')[0]; + } + + /** + * Converts the entry to a plain object + * @returns {Object} Plain object representation + */ + toObject() { + return { + id: this.id, + term: this.term, + definition: this.definition, + category: this.category, + relatedTerms: this.relatedTerms, + dateAdded: this.dateAdded, + dateModified: this.dateModified + }; + } + + /** + * Creates a formatted string representation + * @returns {string} Formatted string + */ + toString() { + return `${this.term}: ${this.definition.substring(0, 100)}...`; + } +} + +export class JargonEntryCollection { + constructor(entries = []) { + this.entries = entries.map(entry => new JargonEntry(entry)); + this.categories = this._buildCategories(); + } + + /** + * Builds categories from entries + * @private + */ + _buildCategories() { + const categories = {}; + this.entries.forEach(entry => { + const category = entry.category; + if (!categories[category]) { + categories[category] = []; + } + categories[category].push(entry); + }); + return categories; + } + + /** + * Adds a new entry + * @param {Object} data - Entry data + * @returns {JargonEntry} The created entry + */ + addEntry(data) { + const entry = new JargonEntry(data); + if (entry.isValid()) { + this.entries.push(entry); + this.categories = this._buildCategories(); // Rebuild categories + return entry; + } + throw new Error('Invalid entry data'); + } + + /** + * Gets an entry by ID + * @param {number} id - Entry ID + * @returns {JargonEntry} The entry or undefined + */ + getEntryById(id) { + return this.entries.find(entry => entry.id === id); + } + + /** + * Gets an entry by term + * @param {string} term - Term to search for + * @returns {JargonEntry} The entry or undefined + */ + getEntryByTerm(term) { + return this.entries.find(entry => + entry.term.toLowerCase() === term.toLowerCase() + ); + } + + /** + * Gets entries by category + * @param {string} category - Category name + * @returns {Array} Array of entries in the category + */ + getEntriesByCategory(category) { + return this.entries.filter(entry => + entry.category.toLowerCase() === category.toLowerCase() + ); + } + + /** + * Gets all categories + * @returns {Array} Array of category names + */ + getCategories() { + return Object.keys(this.categories); + } + + /** + * Gets all entries + * @returns {Array} Array of all entries + */ + getAllEntries() { + return this.entries; + } + + /** + * Removes an entry + * @param {number} id - Entry ID to remove + * @returns {boolean} True if removed + */ + removeEntry(id) { + const index = this.entries.findIndex(entry => entry.id === id); + if (index !== -1) { + this.entries.splice(index, 1); + this.categories = this._buildCategories(); // Rebuild categories + return true; + } + return false; + } + + /** + * Updates an entry + * @param {number} id - Entry ID to update + * @param {Object} newData - New data + * @returns {JargonEntry} Updated entry or undefined + */ + updateEntry(id, newData) { + const entry = this.getEntryById(id); + if (entry) { + entry.update(newData); + this.categories = this._buildCategories(); // Rebuild categories + return entry; + } + return undefined; + } +} \ No newline at end of file diff --git a/src/pages/Home.js b/src/pages/Home.js index 808bf59..07432bf 100644 --- a/src/pages/Home.js +++ b/src/pages/Home.js @@ -1,38 +1,60 @@ import React, { useState, useEffect } from 'react'; import EntryList from '../components/EntryList'; import SearchBar from '../components/SearchBar'; +import { contentLoader } from '../services/contentLoader'; +import { searchEntries } from '../utils/searchUtils'; import './Home.css'; const Home = () => { const [entries, setEntries] = useState([]); const [filteredEntries, setFilteredEntries] = useState([]); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { - // In a real app, this would fetch from an API or data file - // For now, we'll use mock data - const mockEntries = [ - { - id: 1, - term: "Hacker", - definition: "A person who enjoys exploring the details of programmable systems and how to stretch their capabilities.", - category: "Core Terms" - }, - { - id: 2, - term: "Guru", - definition: "A person who has achieved a high level of skill in a particular field or domain.", - category: "Core Terms" - }, - { - id: 3, - term: "Foo", - definition: "A generic term used as a placeholder in examples and documentation.", - category: "Technical Terms" + // Load entries from content loader + const loadEntries = async () => { + try { + // Load the existing data from jargonEntries.json + const response = await fetch('/src/data/jargonEntries.json'); + const data = await response.json(); + + // Store in content loader for future use + await contentLoader.loadContent(JSON.stringify(data), 'json'); + + setEntries(data); + setFilteredEntries(data); + setIsLoading(false); + } catch (error) { + console.error('Error loading entries:', error); + // Fallback to mock data + const mockEntries = [ + { + id: 1, + term: "Hacker", + definition: "A person who enjoys exploring the details of programmable systems and how to stretch their capabilities.", + category: "Core Terms" + }, + { + id: 2, + term: "Guru", + definition: "A person who has achieved a high level of skill in a particular field or domain.", + category: "Core Terms" + }, + { + id: 3, + term: "Foo", + definition: "A generic term used as a placeholder in examples and documentation.", + category: "Technical Terms" + } + ]; + + setEntries(mockEntries); + setFilteredEntries(mockEntries); + setIsLoading(false); } - ]; + }; - setEntries(mockEntries); - setFilteredEntries(mockEntries); + loadEntries(); }, []); const handleSearch = (searchTerm) => { @@ -41,14 +63,22 @@ const Home = () => { return; } - const filtered = entries.filter(entry => - entry.term.toLowerCase().includes(searchTerm.toLowerCase()) || - entry.definition.toLowerCase().includes(searchTerm.toLowerCase()) - ); - + const filtered = searchEntries(entries, searchTerm); setFilteredEntries(filtered); }; + if (isLoading) { + return ( +
+
+

Explore Hacker Culture

+

Discover the terminology that defines computer hacker culture

+
+

Loading jargon entries...

+
+ ); + } + return (
diff --git a/src/pages/SearchResults.js b/src/pages/SearchResults.js new file mode 100644 index 0000000..f3caad2 --- /dev/null +++ b/src/pages/SearchResults.js @@ -0,0 +1,57 @@ +import React, { useState, useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import EntryList from '../components/EntryList'; +import SearchBar from '../components/SearchBar'; +import { contentLoader } from '../services/contentLoader'; +import { searchEntries } from '../utils/searchUtils'; +import './SearchResults.css'; + +const SearchResults = () => { + const [entries, setEntries] = useState([]); + const [filteredEntries, setFilteredEntries] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const location = useLocation(); + + useEffect(() => { + // Load entries from content loader + const loadEntries = async () => { + // In a real app, we'd load from actual data source + // For now, using the existing data in the data file + const entriesData = contentLoader.getAllEntries(); + setEntries(entriesData); + setFilteredEntries(entriesData); + }; + + loadEntries(); + }, []); + + useEffect(() => { + // Handle search from URL parameters + const urlParams = new URLSearchParams(location.search); + const search = urlParams.get('q'); + if (search) { + setSearchTerm(search); + const results = searchEntries(entries, search); + setFilteredEntries(results); + } + }, [location.search, entries]); + + const handleSearch = (searchTerm) => { + setSearchTerm(searchTerm); + const results = searchEntries(entries, searchTerm); + setFilteredEntries(results); + }; + + return ( +
+
+

Search Results

+

Find jargon terms and definitions

+
+ + +
+ ); +}; + +export default SearchResults; \ No newline at end of file diff --git a/src/services/contentLoader.js b/src/services/contentLoader.js new file mode 100644 index 0000000..a3d155d --- /dev/null +++ b/src/services/contentLoader.js @@ -0,0 +1,150 @@ +/** + * Content Loading System for Jargon File + * Handles loading, parsing, and managing jargon entries + */ + +import { parseContent, structureData } from '../utils/contentParser'; +import { JargonEntryCollection } from '../models/JargonEntry'; + +/** + * Content Loader Class + */ +export class ContentLoader { + constructor() { + this.entries = []; + this.collection = null; + this.isLoaded = false; + } + + /** + * Load content from a source + * @param {string} source - Source content + * @param {string} format - Format of the source ('json', 'text', 'html') + * @returns {Promise} True if successful + */ + async loadContent(source, format = 'json') { + try { + const parsedEntries = parseContent(source, format); + this.entries = parsedEntries; + this.collection = new JargonEntryCollection(parsedEntries); + this.isLoaded = true; + return true; + } catch (error) { + console.error('Error loading content:', error); + return false; + } + } + + /** + * Load content from a file (browser environment) + * @param {File} file - File to load + * @returns {Promise} True if successful + */ + async loadFromFile(file) { + try { + const reader = new FileReader(); + const content = await new Promise((resolve, reject) => { + reader.onload = (e) => resolve(e.target.result); + reader.onerror = (e) => reject(e); + reader.readAsText(file); + }); + + const format = this._detectFormat(file.name); + return await this.loadContent(content, format); + } catch (error) { + console.error('Error loading file:', error); + return false; + } + } + + /** + * Load content from URL + * @param {string} url - URL to fetch content from + * @returns {Promise} True if successful + */ + async loadFromURL(url) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const content = await response.text(); + const format = this._detectFormat(url); + return await this.loadContent(content, format); + } catch (error) { + console.error('Error loading from URL:', error); + return false; + } + } + + /** + * Detect format from filename or URL + * @private + * @param {string} source - Source name or URL + * @returns {string} Detected format + */ + _detectFormat(source) { + if (source.endsWith('.json')) { + return 'json'; + } else if (source.endsWith('.txt')) { + return 'text'; + } else if (source.endsWith('.html') || source.endsWith('.htm')) { + return 'html'; + } + return 'json'; // Default + } + + /** + * Get all loaded entries + * @returns {Array} Array of entries + */ + getEntries() { + return this.entries; + } + + /** + * Get the entry collection + * @returns {JargonEntryCollection} The collection + */ + getCollection() { + return this.collection; + } + + /** + * Check if content is loaded + * @returns {boolean} True if loaded + */ + isContentLoaded() { + return this.isLoaded; + } + + /** + * Get categories + * @returns {Array} Array of categories + */ + getCategories() { + return this.collection ? this.collection.getCategories() : []; + } + + /** + * Get entries by category + * @param {string} category - Category name + * @returns {Array} Array of entries + */ + getEntriesByCategory(category) { + return this.collection ? this.collection.getEntriesByCategory(category) : []; + } + + /** + * Get all entries (alias for getEntries) + * @returns {Array} Array of all entries + */ + getAllEntries() { + return this.collection ? this.collection.getAllEntries() : []; + } +} + +/** + * Singleton instance of ContentLoader + */ +export const contentLoader = new ContentLoader(); \ No newline at end of file diff --git a/src/utils/contentParser.js b/src/utils/contentParser.js new file mode 100644 index 0000000..9adc189 --- /dev/null +++ b/src/utils/contentParser.js @@ -0,0 +1,149 @@ +/** + * Content Parser for Jargon File entries + * This utility handles parsing content from various formats into structured data + */ + +/** + * Parse JSON format jargon entries + * @param {string} content - JSON string containing jargon entries + * @returns {Array} Array of parsed jargon entries + */ +export const parseJSON = (content) => { + try { + const data = JSON.parse(content); + return data.map(entry => ({ + id: entry.id, + term: entry.term, + definition: entry.definition, + category: entry.category || 'Uncategorized', + relatedTerms: entry.relatedTerms || [], + dateAdded: entry.dateAdded || new Date().toISOString().split('T')[0] + })); + } catch (error) { + console.error('Error parsing JSON:', error); + return []; + } +}; + +/** + * Parse text format jargon entries (simple format) + * @param {string} content - Text content with entries + * @returns {Array} Array of parsed jargon entries + */ +export const parseText = (content) => { + // Simple text parsing logic + const entries = []; + const lines = content.split('\n'); + let currentEntry = null; + + lines.forEach(line => { + if (line.trim().startsWith('TERM:')) { + if (currentEntry) { + entries.push(currentEntry); + } + currentEntry = { + term: line.replace('TERM:', '').trim(), + definition: '', + category: 'Uncategorized', + relatedTerms: [], + dateAdded: new Date().toISOString().split('T')[0] + }; + } else if (line.trim().startsWith('DEFINITION:')) { + if (currentEntry) { + currentEntry.definition = line.replace('DEFINITION:', '').trim(); + } + } else if (line.trim().startsWith('CATEGORY:')) { + if (currentEntry) { + currentEntry.category = line.replace('CATEGORY:', '').trim(); + } + } else if (line.trim().startsWith('RELATED:')) { + if (currentEntry) { + currentEntry.relatedTerms = line.replace('RELATED:', '').trim().split(',').map(term => term.trim()); + } + } + }); + + if (currentEntry) { + entries.push(currentEntry); + } + + return entries; +}; + +/** + * Parse HTML format jargon entries + * @param {string} content - HTML content containing jargon entries + * @returns {Array} Array of parsed jargon entries + */ +export const parseHTML = (content) => { + // For now, we'll provide a basic structure - in practice this would parse HTML + // This is a simplified version that would need to be expanded + const entries = []; + + // This is a placeholder - would need more sophisticated HTML parsing + // For the purpose of this implementation, we'll just return empty entries + // In a real implementation, we would parse HTML tags like , , etc. + + return entries; +}; + +/** + * Generic parse function that tries to determine the format and parse accordingly + * @param {string} content - Content to parse + * @param {string} format - Format to parse as ('json', 'text', 'html') + * @returns {Array} Array of parsed jargon entries + */ +export const parseContent = (content, format) => { + switch (format.toLowerCase()) { + case 'json': + return parseJSON(content); + case 'text': + return parseText(content); + case 'html': + return parseHTML(content); + default: + // Try to auto-detect format + try { + JSON.parse(content); + return parseJSON(content); + } catch { + return parseText(content); + } + } +}; + +/** + * Structure data for efficient storage and retrieval + * @param {Array} entries - Array of jargon entries + * @returns {Object} Structured data with categories and indexes + */ +export const structureData = (entries) => { + const categories = {}; + const termIndex = {}; + const categoryIndex = {}; + + entries.forEach(entry => { + // Build category mapping + const category = entry.category; + if (!categories[category]) { + categories[category] = []; + } + categories[category].push(entry.id); + + // Build term index for fast lookup + termIndex[entry.term.toLowerCase()] = entry.id; + + // Build category index + if (!categoryIndex[category]) { + categoryIndex[category] = []; + } + categoryIndex[category].push(entry.id); + }); + + return { + entries, + categories, + termIndex, + categoryIndex + }; +}; \ No newline at end of file diff --git a/src/utils/searchUtils.js b/src/utils/searchUtils.js new file mode 100644 index 0000000..599fc93 --- /dev/null +++ b/src/utils/searchUtils.js @@ -0,0 +1,108 @@ +/** + * Search Utilities for Jargon File + * Handles searching through jargon entries + */ + +/** + * Search through jargon entries + * @param {Array} entries - Array of jargon entries + * @param {string} searchTerm - Search term + * @returns {Array} Filtered entries + */ +export const searchEntries = (entries, searchTerm) => { + if (!searchTerm || searchTerm.trim() === '') { + return entries; + } + + const term = searchTerm.toLowerCase().trim(); + + return entries.filter(entry => { + // Search in term, definition, and related terms + const termMatch = entry.term.toLowerCase().includes(term); + const definitionMatch = entry.definition.toLowerCase().includes(term); + const relatedTermsMatch = entry.relatedTerms.some(term => + term.toLowerCase().includes(term) + ); + + return termMatch || definitionMatch || relatedTermsMatch; + }); +}; + +/** + * Advanced search with category filtering + * @param {Array} entries - Array of jargon entries + * @param {string} searchTerm - Search term + * @param {string} category - Category to filter by (optional) + * @returns {Array} Filtered entries + */ +export const advancedSearch = (entries, searchTerm, category = null) => { + let results = entries; + + // Apply category filter if specified + if (category && category !== 'All') { + results = results.filter(entry => + entry.category.toLowerCase() === category.toLowerCase() + ); + } + + // Apply search term filter + if (searchTerm && searchTerm.trim() !== '') { + results = searchEntries(results, searchTerm); + } + + return results; +}; + +/** + * Get search suggestions based on current entries + * @param {Array} entries - Array of jargon entries + * @param {string} searchTerm - Current search term + * @returns {Array} Suggested terms + */ +export const getSearchSuggestions = (entries, searchTerm) => { + if (!searchTerm || searchTerm.trim() === '') { + return []; + } + + const term = searchTerm.toLowerCase().trim(); + const suggestions = new Set(); + + entries.forEach(entry => { + // Add exact matches + if (entry.term.toLowerCase().startsWith(term)) { + suggestions.add(entry.term); + } + + // Add partial matches in definition + if (entry.definition.toLowerCase().includes(term)) { + // Extract potential terms from definition for suggestions + // This is a simple implementation - more sophisticated would use NLP + const words = entry.definition.toLowerCase().split(/\s+/); + words.forEach(word => { + if (word.startsWith(term) && word.length > 2) { + suggestions.add(word); + } + }); + } + }); + + return Array.from(suggestions).slice(0, 5); // Return top 5 suggestions +}; + +/** + * Get search statistics + * @param {Array} entries - Array of jargon entries + * @param {string} searchTerm - Search term + * @returns {Object} Search statistics + */ +export const getSearchStats = (entries, searchTerm) => { + const totalEntries = entries.length; + const filteredEntries = searchEntries(entries, searchTerm); + + return { + totalEntries, + filteredEntries: filteredEntries.length, + searchTerm, + searchRatio: totalEntries > 0 ? (filteredEntries.length / totalEntries * 100).toFixed(1) : 0 + }; +}; \ No newline at end of file