UNPKG

lorehub

Version:

Capture and surface the collective wisdom of your codebase

275 lines 14 kB
import React, { useState, useEffect } from 'react'; import { Box, Text, 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 { getRealmInfo } from '../utils/realm.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 AddLore({ db, realmPath, initialContent = '', initialType = 'decree', initialWhy = '', initialProvinces = [], initialSigils = [], initialConfidence = 80, onComplete, }) { 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 || 90); // Default to 90% const [duplicates, setDuplicates] = useState([]); const [showDuplicateWarning, setShowDuplicateWarning] = useState(false); // Load or create realm useEffect(() => { async function loadRealm() { try { const realmInfo = await getRealmInfo(realmPath); let existingRealm = db.findRealmByPath(realmPath); if (!existingRealm) { existingRealm = db.createRealm({ name: realmInfo.name, path: realmInfo.path, gitRemote: realmInfo.gitRemote, isMonorepo: realmInfo.isMonorepo, provinces: realmInfo.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, realmPath]); 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); // Exit immediately after successful creation onComplete?.(true); process.exit(0); } 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) - simplified for essential fields only const fields = content ? ['content', 'type', 'confidence'] : ['content']; const currentIndex = fields.indexOf(currentField); const nextIndex = (currentIndex + 1) % fields.length; setCurrentField(fields[nextIndex]); } else if (key.tab && key.shift) { // Shift-Tab navigation (backward) - simplified for essential fields only const fields = content ? ['content', 'type', 'confidence'] : ['content']; 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); process.exit(0); } 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)); } } }); // Remove the success screen - we exit immediately after creation if (!loading && 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 (!loading && 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 - conversational style return (React.createElement(AlternativeScreenView, null, React.createElement(Box, { flexDirection: "column", paddingX: 2, paddingY: 1 }, error && !realm && (React.createElement(Box, { marginBottom: 1 }, React.createElement(Text, { color: "red" }, "\u26A0 ", error))), React.createElement(Box, { flexDirection: "column" }, React.createElement(Text, { color: currentField === 'content' ? 'cyan' : undefined }, "\uD83D\uDCDD What lore do you wish to record?"), React.createElement(Box, { marginTop: 1, flexDirection: "row" }, React.createElement(Text, { color: "gray" }, "> "), currentField === 'content' ? (React.createElement(TextInput, { value: content, onChange: setContent, placeholder: "", focus: true })) : (React.createElement(Text, null, content)))), content && (React.createElement(React.Fragment, null, React.createElement(Box, { flexDirection: "column", marginTop: 1 }, React.createElement(Text, { color: currentField === 'type' ? 'cyan' : undefined }, "\uD83D\uDCC2 Type: ", loreTypes.find(t => t.value === type)?.label?.toLowerCase()), currentField === 'type' && (React.createElement(Box, { marginTop: 1, marginLeft: 2 }, React.createElement(SelectInput, { items: loreTypes.map(t => ({ ...t, label: t.label.toLowerCase(), })), onSelect: (item) => setType(item.value), initialIndex: loreTypes.findIndex(t => t.value === type), limit: 8 })))), React.createElement(Box, { flexDirection: "column", marginTop: 1 }, React.createElement(Text, { color: currentField === 'confidence' ? 'cyan' : undefined }, "\u2728 Confidence: ", confidence, "%"), currentField === 'confidence' && (React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { dimColor: true }, "Use \u2190 \u2192 arrows to adjust")))))), isSubmitting && (React.createElement(Box, { marginTop: 2 }, React.createElement(Text, { color: "green" }, "\u2713 Lore recorded successfully!"))), error && (React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: "red" }, "\u26A0 ", error))), React.createElement(Box, { marginTop: 2 }, React.createElement(Text, { dimColor: true }, currentField === 'content' ? 'Press Enter to continue' : currentField === 'confidence' ? 'Press Enter to save, Tab to continue' : 'Press Tab to continue, Enter to save'))))); } //# sourceMappingURL=AddLore.js.map