Initial commit: RA-H Open Source Edition
Local-first knowledge management system with BYO API keys. Features: - 3-panel UI (Nodes | Focus | Helpers) - SQLite + sqlite-vec for vector search - Agent system (Easy/Hard mode orchestrators) - Content extraction (YouTube, PDF, web) - Integrate workflow for connection discovery - Dimension system with auto-assignment Tech stack: - Next.js 15 + TypeScript + Tailwind CSS - Anthropic (Claude) + OpenAI (GPT) via Vercel AI SDK Setup: npm install && npm rebuild better-sqlite3 scripts/dev/bootstrap-local.sh npm run dev MIT License
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { CSSProperties, useState } from 'react';
|
||||
|
||||
interface ChipProps {
|
||||
label: string;
|
||||
onRemove?: () => void;
|
||||
color?: string;
|
||||
maxWidth?: number;
|
||||
icon?: React.ReactNode;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export default function Chip({
|
||||
label,
|
||||
onRemove,
|
||||
color = '#1a1a1a',
|
||||
maxWidth = 150,
|
||||
icon,
|
||||
style = {}
|
||||
}: ChipProps) {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const needsTruncation = label.length > 20;
|
||||
const displayLabel = needsTruncation ? label.slice(0, 18) + '...' : label;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
...style
|
||||
}}
|
||||
onMouseEnter={() => needsTruncation && setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
background: color,
|
||||
color: '#fff',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '10px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
maxWidth: `${maxWidth}px`,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<span style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{displayLabel}
|
||||
</span>
|
||||
{onRemove && (
|
||||
<button
|
||||
onClick={onRemove}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#ccc',
|
||||
cursor: 'pointer',
|
||||
fontSize: '8px',
|
||||
padding: '0',
|
||||
lineHeight: 1,
|
||||
marginLeft: '2px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
transition: 'background 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'none';
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* Tooltip */}
|
||||
{showTooltip && needsTruncation && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '100%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
marginBottom: '4px',
|
||||
padding: '4px 8px',
|
||||
background: '#333',
|
||||
color: '#fff',
|
||||
fontSize: '11px',
|
||||
borderRadius: '3px',
|
||||
whiteSpace: 'nowrap',
|
||||
zIndex: 1000,
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.5)',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function ConfirmDialog({
|
||||
open,
|
||||
title,
|
||||
message,
|
||||
confirmLabel = 'Confirm',
|
||||
cancelLabel = 'Cancel',
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmDialogProps) {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999,
|
||||
padding: '20px'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '380px',
|
||||
maxWidth: '100%',
|
||||
background: '#050505',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
boxShadow: '0 25px 65px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
color: '#f8fafc',
|
||||
marginBottom: '12px',
|
||||
letterSpacing: '0.02em'
|
||||
}}>
|
||||
{title}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
color: '#94a3b8',
|
||||
marginBottom: '24px',
|
||||
lineHeight: 1.6
|
||||
}}>
|
||||
{message}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
style={{
|
||||
padding: '10px 16px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #1f1f1f',
|
||||
background: 'transparent',
|
||||
color: '#94a3b8',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#0f0f0f';
|
||||
e.currentTarget.style.borderColor = '#2a2a2a';
|
||||
e.currentTarget.style.color = '#cbd5f5';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.borderColor = '#1f1f1f';
|
||||
e.currentTarget.style.color = '#94a3b8';
|
||||
}}
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
style={{
|
||||
padding: '10px 16px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #dc2626',
|
||||
background: '#7f1d1d',
|
||||
color: '#fca5a5',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#991b1b';
|
||||
e.currentTarget.style.borderColor = '#b91c1c';
|
||||
e.currentTarget.style.color = '#fecaca';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#7f1d1d';
|
||||
e.currentTarget.style.borderColor = '#dc2626';
|
||||
e.currentTarget.style.color = '#fca5a5';
|
||||
}}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, CSSProperties } from 'react';
|
||||
|
||||
interface EditableSectionProps {
|
||||
label: string;
|
||||
summary: ReactNode;
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
children: ReactNode;
|
||||
metadata?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function EditableSection({
|
||||
label,
|
||||
summary,
|
||||
expanded,
|
||||
onToggle,
|
||||
children,
|
||||
metadata,
|
||||
disabled = false
|
||||
}: EditableSectionProps) {
|
||||
return (
|
||||
<div style={{
|
||||
marginBottom: '12px',
|
||||
background: '#1a1a1a',
|
||||
border: '1px solid #2a2a2a',
|
||||
borderRadius: '4px',
|
||||
overflow: 'visible'
|
||||
}}>
|
||||
<div
|
||||
onClick={disabled ? undefined : onToggle}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
cursor: disabled ? 'default' : 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
transition: 'background 0.2s',
|
||||
background: expanded ? '#202020' : 'transparent'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!disabled && !expanded) {
|
||||
e.currentTarget.style.background = '#1f1f1f';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!disabled && !expanded) {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
color: '#5c9aff',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
minWidth: '40px'
|
||||
}}>
|
||||
{label}
|
||||
</span>
|
||||
|
||||
<div style={{
|
||||
flex: 1,
|
||||
fontSize: '11px',
|
||||
color: '#888',
|
||||
lineHeight: '1.4',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{summary}
|
||||
</div>
|
||||
|
||||
{metadata && (
|
||||
<span style={{
|
||||
fontSize: '10px',
|
||||
color: '#555',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{metadata}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!disabled && (
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
style={{
|
||||
color: '#555',
|
||||
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.2s'
|
||||
}}
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{disabled && (
|
||||
<span style={{
|
||||
fontSize: '9px',
|
||||
color: '#555',
|
||||
background: '#252525',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '2px'
|
||||
}}>
|
||||
locked
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
borderTop: '1px solid #2a2a2a',
|
||||
background: '#161616',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
interface InputDialogProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
onConfirm: (value: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function InputDialog({
|
||||
open,
|
||||
title,
|
||||
message,
|
||||
placeholder = '',
|
||||
defaultValue = '',
|
||||
confirmLabel = 'Confirm',
|
||||
cancelLabel = 'Cancel',
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: InputDialogProps) {
|
||||
const [inputValue, setInputValue] = useState(defaultValue);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setInputValue(defaultValue);
|
||||
// Focus input when dialog opens
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, 100);
|
||||
}
|
||||
}, [open, defaultValue]);
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (inputValue.trim()) {
|
||||
onConfirm(inputValue.trim());
|
||||
setInputValue('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleConfirm();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999,
|
||||
padding: '20px'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onCancel();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '380px',
|
||||
maxWidth: '100%',
|
||||
background: '#050505',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
boxShadow: '0 25px 65px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
color: '#f8fafc',
|
||||
marginBottom: '12px',
|
||||
letterSpacing: '0.02em'
|
||||
}}>
|
||||
{title}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
color: '#94a3b8',
|
||||
marginBottom: '16px',
|
||||
lineHeight: 1.6
|
||||
}}>
|
||||
{message}
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
background: '#0f0f0f',
|
||||
border: '1px solid #2a2a2a',
|
||||
borderRadius: '8px',
|
||||
color: '#f8fafc',
|
||||
fontSize: '13px',
|
||||
marginBottom: '24px',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s'
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#3a3a3a';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#2a2a2a';
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
style={{
|
||||
padding: '10px 16px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #1f1f1f',
|
||||
background: 'transparent',
|
||||
color: '#94a3b8',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#0f0f0f';
|
||||
e.currentTarget.style.borderColor = '#2a2a2a';
|
||||
e.currentTarget.style.color = '#cbd5f5';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.borderColor = '#1f1f1f';
|
||||
e.currentTarget.style.color = '#94a3b8';
|
||||
}}
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={!inputValue.trim()}
|
||||
style={{
|
||||
padding: '10px 16px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #22c55e',
|
||||
background: inputValue.trim() ? '#1a3529' : '#0f1a15',
|
||||
color: inputValue.trim() ? '#7de8a5' : '#4a5a4f',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
cursor: inputValue.trim() ? 'pointer' : 'not-allowed',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (inputValue.trim()) {
|
||||
e.currentTarget.style.background = '#1f3d2f';
|
||||
e.currentTarget.style.borderColor = '#2dd47e';
|
||||
e.currentTarget.style.color = '#9ef5b8';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (inputValue.trim()) {
|
||||
e.currentTarget.style.background = '#1a3529';
|
||||
e.currentTarget.style.borderColor = '#22c55e';
|
||||
e.currentTarget.style.color = '#7de8a5';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, CSSProperties } from 'react';
|
||||
|
||||
interface PanelHeaderProps {
|
||||
title: string;
|
||||
leftContent?: ReactNode;
|
||||
rightContent?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export default function PanelHeader({
|
||||
title,
|
||||
leftContent,
|
||||
rightContent,
|
||||
className = '',
|
||||
style = {}
|
||||
}: PanelHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
className={`panel-header ${className}`}
|
||||
style={{
|
||||
height: '48px',
|
||||
padding: '0 12px',
|
||||
borderBottom: '1px solid rgba(42, 42, 42, 0.8)',
|
||||
background: '#0f0f0f',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexShrink: 0,
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
flex: 1,
|
||||
minWidth: 0
|
||||
}}>
|
||||
<span style={{
|
||||
textTransform: 'uppercase',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
color: '#888',
|
||||
letterSpacing: '0.05em',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{title}
|
||||
</span>
|
||||
{leftContent}
|
||||
</div>
|
||||
|
||||
{rightContent && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{rightContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user