UNPKG

lorehub

Version:

Capture and surface the collective wisdom of your codebase

309 lines 16.8 kB
import React, { useState, useEffect } from 'react'; import { Box, Text, useApp, useInput } from 'ink'; import TextInput from 'ink-text-input'; import SelectInput from 'ink-select-input'; import { AlternativeScreenView } from './AlternativeScreenView.js'; import { useTerminalDimensions } from '../hooks/useTerminalDimensions.js'; import { getProjectInfo } from '../utils/project.js'; const loreTypes = [ { label: 'Decree', value: 'decree' }, { label: 'Wisdom', value: 'wisdom' }, { label: 'Belief', value: 'belief' }, { label: 'Constraint', value: 'constraint' }, { label: 'Requirement', value: 'requirement' }, { label: 'Risk', value: 'risk' }, { label: 'Quest', value: 'quest' }, { label: 'Saga', value: 'saga' }, { label: 'Story', value: 'story' }, { label: 'Anomaly', value: 'anomaly' }, { label: 'Other', value: 'other' }, ]; const typeDescriptions = { decree: 'Architectural or technical choice', wisdom: 'Something discovered or learned', belief: 'Unverified belief or hypothesis', constraint: 'Limitation or restriction', requirement: 'Business or technical requirement', risk: 'Potential problem or concern', quest: 'Future action needed', saga: 'Major initiative that will generate many lores', story: 'User story', anomaly: 'Bug or issue', other: 'Miscellaneous lore', }; export function AddFact({ db, projectPath, initialContent = '', initialType = 'decree', initialWhy = '', initialProvinces = [], initialSigils = [], initialConfidence = 80, onComplete, }) { const { exit } = useApp(); const { columns, rows } = useTerminalDimensions(); const [realm, setRealm] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const [showHelp, setShowHelp] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); // Form state const [currentField, setCurrentField] = useState('content'); const [content, setContent] = useState(initialContent); const [type, setType] = useState(initialType); const [why, setWhy] = useState(initialWhy); const [provincesText, setProvincesText] = useState(initialProvinces.join(', ')); const [sigilsText, setSigilsText] = useState(initialSigils.join(', ')); const [confidence, setConfidence] = useState(initialConfidence); const [duplicates, setDuplicates] = useState([]); const [showDuplicateWarning, setShowDuplicateWarning] = useState(false); // Load or create realm useEffect(() => { async function loadRealm() { try { const projectInfo = await getProjectInfo(projectPath); let existingRealm = db.findRealmByPath(projectPath); if (!existingRealm) { existingRealm = db.createRealm({ name: projectInfo.name, path: projectInfo.path, gitRemote: projectInfo.gitRemote, isMonorepo: projectInfo.isMonorepo, provinces: projectInfo.provinces, }); } else { db.updateRealmLastSeen(existingRealm.id); } setRealm(existingRealm); setLoading(false); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load realm'); setLoading(false); } } loadRealm(); }, [db, projectPath]); const handleSubmit = async () => { if (isSubmitting || success) { return; // Prevent duplicate submissions } if (!realm || !content.trim()) { setError('Content is required'); setTimeout(() => setError(null), 3000); return; } setIsSubmitting(true); try { // Check for duplicates before creating if (!showDuplicateWarning) { const potentialDuplicates = await db.checkForDuplicates(content.trim(), realm.id, 0.85 // 85% similarity threshold ); if (potentialDuplicates.length > 0) { setDuplicates(potentialDuplicates.map(d => ({ content: d.content, similarity: d.similarity }))); setShowDuplicateWarning(true); setIsSubmitting(false); return; } } const loreInput = { realmId: realm.id, content: content.trim(), type, why: why.trim() || undefined, provinces: provincesText.split(',').map((s) => s.trim()).filter(Boolean), sigils: sigilsText.split(',').map((t) => t.trim()).filter(Boolean), confidence, origin: { type: 'manual', reference: 'cli', context: `Added via CLI in ${realm.name}`, }, }; const lore = await db.createLore(loreInput); setSuccess(true); setTimeout(() => { onComplete?.(true); exit(); }, 1500); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create lore'); setTimeout(() => setError(null), 3000); setIsSubmitting(false); } }; // Handle keyboard input useInput((input, key) => { // Ignore all input during submission or after success if (isSubmitting || success) { return; } // Handle duplicate warning response if (showDuplicateWarning) { if (input.toLowerCase() === 'y') { setShowDuplicateWarning(false); handleSubmit(); // Continue with submission } else if (input.toLowerCase() === 'n' || key.escape) { setShowDuplicateWarning(false); setDuplicates([]); } return; } if (showHelp) { if (input === '?' || key.escape) { setShowHelp(false); } return; } // Check if we're in a text input field const isTextInputField = currentField === 'content' || currentField === 'why' || currentField === 'provinces' || currentField === 'sigils'; if (input === '?' && !isTextInputField) { setShowHelp(true); } else if (key.tab && !key.shift) { // Tab navigation (forward) const fields = ['content', 'type', 'why']; if (realm?.isMonorepo) fields.push('provinces'); fields.push('sigils', 'confidence'); const currentIndex = fields.indexOf(currentField); const nextIndex = (currentIndex + 1) % fields.length; setCurrentField(fields[nextIndex]); } else if (key.tab && key.shift) { // Shift-Tab navigation (backward) const fields = ['content', 'type', 'why']; if (realm?.isMonorepo) fields.push('provinces'); fields.push('sigils', 'confidence'); const currentIndex = fields.indexOf(currentField); const prevIndex = currentIndex === 0 ? fields.length - 1 : currentIndex - 1; setCurrentField(fields[prevIndex]); } else if (key.return && !key.shift) { handleSubmit(); } else if (key.escape) { onComplete?.(false); exit(); } else if (currentField === 'confidence') { if (key.leftArrow && confidence > 0) { setConfidence(Math.max(0, confidence - 5)); } else if (key.rightArrow && confidence < 100) { setConfidence(Math.min(100, confidence + 5)); } } }); // Handle loading and error states if (loading) { return (React.createElement(AlternativeScreenView, null, React.createElement(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: rows - 1 }, React.createElement(Text, null, "Loading realm information...")))); } if (success) { return (React.createElement(AlternativeScreenView, null, React.createElement(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: rows - 1 }, React.createElement(Text, { color: "green", bold: true }, "\u2713 Lore created successfully!"), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { dimColor: true }, "Returning to shell..."))))); } if (showHelp) { return (React.createElement(AlternativeScreenView, null, React.createElement(Box, { flexDirection: "column", height: rows - 1, padding: 1 }, React.createElement(Text, { bold: true, underline: true }, "Keyboard Shortcuts"), React.createElement(Box, { marginTop: 1, flexDirection: "column" }, React.createElement(Text, null, React.createElement(Text, { color: "cyan" }, "Tab"), " - Next field"), React.createElement(Text, null, React.createElement(Text, { color: "cyan" }, "Shift+Tab"), " - Previous field"), React.createElement(Text, null, React.createElement(Text, { color: "cyan" }, "Enter"), " - Save lore"), React.createElement(Text, null, React.createElement(Text, { color: "cyan" }, "Esc"), " - Cancel"), React.createElement(Text, null, React.createElement(Text, { color: "cyan" }, "\u2190/\u2192"), " - Adjust confidence (when in confidence field)"), React.createElement(Text, null, React.createElement(Text, { color: "cyan" }, "?"), " - Toggle this help")), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { dimColor: true }, "Press ? or Esc to return"))))); } // Show duplicate warning if needed if (showDuplicateWarning && duplicates.length > 0) { return (React.createElement(AlternativeScreenView, null, React.createElement(Box, { flexDirection: "column", height: rows - 1, padding: 1 }, React.createElement(Text, { bold: true, color: "yellow" }, "\u26A0 Potential Duplicate Lores Found"), React.createElement(Box, { marginTop: 1, flexDirection: "column" }, React.createElement(Text, null, "The following existing lores are similar to your new lore:"), React.createElement(Box, { marginTop: 1, flexDirection: "column" }, duplicates.slice(0, 3).map((dup, i) => (React.createElement(Box, { key: i, marginBottom: 1 }, React.createElement(Text, null, "\u2022 [", Math.round(dup.similarity * 100), "% similar] ", dup.content)))))), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, null, "Do you want to continue adding this lore anyway?"), React.createElement(Text, { dimColor: true }, "Press Y to continue, N to cancel"))))); } // Main form - single column layout return (React.createElement(AlternativeScreenView, null, React.createElement(Box, { flexDirection: "column", height: rows - 1 }, React.createElement(Box, { height: 3, flexDirection: "column" }, React.createElement(Text, { bold: true }, "Add New Lore"), React.createElement(Text, { dimColor: true }, realm?.name, " - ", realm?.path), error && React.createElement(Text, { color: "red" }, "\u26A0 ", error)), React.createElement(Box, { flexDirection: "column", height: rows - 8, paddingX: 1 }, React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, React.createElement(Text, { bold: true, color: currentField === 'content' ? 'cyan' : undefined }, "Content ", !content && React.createElement(Text, { dimColor: true }, "(required)")), currentField === 'content' ? (React.createElement(TextInput, { value: content, onChange: setContent, placeholder: "e.g., Use Redis for session storage" })) : (React.createElement(Text, { color: content ? undefined : 'gray' }, content || 'No content yet'))), React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, React.createElement(Text, { bold: true, color: currentField === 'type' ? 'cyan' : undefined }, "Type"), currentField === 'type' ? (React.createElement(Box, { height: 8 }, React.createElement(SelectInput, { items: loreTypes.map(t => ({ ...t, label: `${t.label} - ${typeDescriptions[t.value]}`, })), onSelect: (item) => setType(item.value), initialIndex: loreTypes.findIndex(t => t.value === type), limit: 8 }))) : (React.createElement(Text, null, loreTypes.find(t => t.value === type)?.label, " - ", React.createElement(Text, { dimColor: true }, typeDescriptions[type])))), React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, React.createElement(Text, { bold: true, color: currentField === 'why' ? 'cyan' : undefined }, "Why ", React.createElement(Text, { dimColor: true }, "(optional)")), currentField === 'why' ? (React.createElement(TextInput, { value: why, onChange: setWhy, placeholder: "e.g., Need sub-50ms session lookups" })) : (React.createElement(Text, { color: why ? undefined : 'gray' }, why || 'No reason provided'))), realm?.isMonorepo && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, React.createElement(Text, { bold: true, color: currentField === 'provinces' ? 'cyan' : undefined }, "Provinces ", React.createElement(Text, { dimColor: true }, "(comma-separated)")), currentField === 'provinces' ? (React.createElement(TextInput, { value: provincesText, onChange: setProvincesText, placeholder: `e.g., ${realm.provinces.slice(0, 3).join(', ')}` })) : (React.createElement(Text, { color: provincesText ? undefined : 'gray' }, provincesText || 'All provinces')))), React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, React.createElement(Text, { bold: true, color: currentField === 'sigils' ? 'cyan' : undefined }, "Sigils ", React.createElement(Text, { dimColor: true }, "(comma-separated)")), currentField === 'sigils' ? (React.createElement(TextInput, { value: sigilsText, onChange: setSigilsText, placeholder: "e.g., redis, cache, performance" })) : (React.createElement(Text, { color: sigilsText ? undefined : 'gray' }, sigilsText || 'No sigils'))), React.createElement(Box, { flexDirection: "column" }, React.createElement(Text, { bold: true, color: currentField === 'confidence' ? 'cyan' : undefined }, "Confidence: ", confidence, "%"), React.createElement(Box, null, React.createElement(Text, null, '[' + '█'.repeat(Math.floor(confidence / 5)) + '░'.repeat(20 - Math.floor(confidence / 5)) + ']')), currentField === 'confidence' && (React.createElement(Text, { dimColor: true }, "Use \u2190 \u2192 to adjust")))), React.createElement(Box, { height: 2 }, React.createElement(Text, { dimColor: true }, "Tab: next | Shift+Tab: previous | Enter: save | Esc: cancel | ?: help"))))); } //# sourceMappingURL=AddFact.js.map