lorehub
Version:
Capture and surface the collective wisdom of your codebase
275 lines • 14 kB
JavaScript
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