Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { initDb } from '../src/db/client.js';
import { repository } from '../src/db/repository.js';
import type { Claim, Note } from '../src/lib/validation.js';
import { escapeHtml } from '../src/lib/security.js';
import type { Claim, Note } from '../src/lib/validation';

const program = new Command();

Expand Down Expand Up @@ -146,7 +145,6 @@ async function exportSite(outDir: string) {
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline';">
<title>Knowledge Base</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
Expand All @@ -170,17 +168,17 @@ async function exportSite(outDir: string) {
const safeId = entity.name.replace(/[^a-z0-9]/gi, '_').toLowerCase();

html += `\n <div class="entity" id="${safeId}">\n`;
html += ` <h2><a href="#${safeId}">${escapeHtml(entity.name)}</a></h2>\n`;
html += ` <span class="type">${escapeHtml(entity.type)}</span>\n`;
html += ` <h2><a href="#${safeId}">${entity.name}</a></h2>\n`;
html += ` <span class="type">${entity.type}</span>\n`;

if (entity.description) {
html += `\n <p>${escapeHtml(entity.description)}</p>\n`;
html += `\n <p>${entity.description}</p>\n`;
}

if (claims.length > 0) {
html += `\n <h3>Claims</h3>\n`;
for (const claim of claims) {
html += ` <div class="claim">${escapeHtml(claim.statement)}`;
html += ` <div class="claim">${claim.statement}`;
if (claim.confidence !== 1) html += ` <em>(confidence: ${claim.confidence})</em>`;
html += `</div>\n`;
}
Expand Down
20 changes: 6 additions & 14 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { MEDIA_QUERIES } from '../lib/constants';
import { useFocusTrap } from '../hooks/useFocusTrap';
import { useEscapeKey } from '../hooks/useEscapeKey';

const GraphControls = lazy(() => import('../features/graph/GraphControls'));
const GraphView = lazy(() => import('../features/graph/GraphView'));
const MindMapView = lazy(() => import('../features/mindmap/MindMapView'));
const Chat = lazy(() => import('../features/chat/Chat'));
Expand Down Expand Up @@ -124,6 +123,12 @@ const AppContent: React.FC = () => {
<MindMapView
rootEntity={entities[0]}
relatedEntities={entities.slice(1, 10)}
entities={entities}
links={links}
onEntityClick={(id) => {
setGraphSelectedNode(id);
setCurrentView('graph');
}}
/>
)}
{dbReady && currentView === 'mindmap' && entities.length === 0 && (
Expand All @@ -148,19 +153,6 @@ const AppContent: React.FC = () => {
onSearchClick={() => setIsSearchOpen(true)}
onClose={() => setIsMenuOpen(false)}
/>
{currentView === 'graph' && (
<div className="drawer-extra-controls">
<h3>Graph Controls</h3>
<Suspense fallback={<div>Loading controls...</div>}>
<GraphControls
focusMode={graphFocusMode}
setFocusMode={setGraphFocusMode}
hasSelection={!!graphSelectedNode}
selectedName={entities.find(e => e.id === graphSelectedNode)?.name}
/>
</Suspense>
</div>
)}
</MobileDrawer>

{isSearchOpen && (
Expand Down
2 changes: 1 addition & 1 deletion src/db/connection-pool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { logger } from '../lib/logger.js';
import { logger } from '../lib/logger';

export const DEFAULT_POOL_SIZE = 4;
const DEFAULT_TIMEOUT_MS = 30000; // 30 seconds default timeout
Expand Down
8 changes: 4 additions & 4 deletions src/db/repository.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getDb, SQLiteDB } from './client.js';
import { Entity, Claim, Note, Link, GraphSnapshot } from '../lib/validation.js';
import { AppError } from '../lib/errors.js';
import { logger } from '../lib/logger.js';
import { getDb, SQLiteDB } from './client';
import { Entity, Claim, Note, Link, GraphSnapshot } from '../lib/validation';
import { AppError } from '../lib/errors';
import { logger } from '../lib/logger';

export interface GraphSnapshotDiff {
added_nodes: string[];
Expand Down
66 changes: 41 additions & 25 deletions src/features/graph/GraphControls.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { useState, useRef } from 'react';
import { Focus, Camera, Clock } from 'lucide-react';
import { Focus, Camera, Clock, X } from 'lucide-react';
import { useFocusTrap } from '../../hooks/useFocusTrap';
import { useEscapeKey } from '../../hooks/useEscapeKey';
import { useMediaQuery } from '../../hooks/useMediaQuery';
import { MEDIA_QUERIES } from '../../lib/constants';

interface GraphNode {
id: string;
Expand Down Expand Up @@ -38,6 +40,7 @@ const GraphControls: React.FC<GraphControlsProps> = ({
const [snapshotName, setSnapshotName] = useState('');
const [snapshotDesc, setSnapshotDesc] = useState('');
const modalRef = useRef<HTMLDivElement>(null);
const isMobile = useMediaQuery(MEDIA_QUERIES.MOBILE);

useFocusTrap(modalRef, showSaveModal);
useEscapeKey(() => setShowSaveModal(false), showSaveModal);
Expand All @@ -50,31 +53,36 @@ const GraphControls: React.FC<GraphControlsProps> = ({
setSnapshotDesc('');
};

return (
<>
<div className="viz-controls">
const controls = (
<div className={isMobile ? "viz-controls-mobile" : "viz-controls"}>
<button
onClick={() => setFocusMode(!focusMode)}
className={focusMode ? 'active' : ''}
disabled={!hasSelection}
aria-pressed={focusMode}
title={!hasSelection ? "Select a node first" : "Toggle Neighborhood Focus"}
>
<Focus size={16} /> {focusMode ? 'Show All' : 'Focus Neighborhood'}
</button>
{onSaveSnapshot && (
<button
onClick={() => setFocusMode(!focusMode)}
className={focusMode ? 'active' : ''}
disabled={!hasSelection}
title={!hasSelection ? "Select a node first" : "Toggle Neighborhood Focus"}
onClick={() => setShowSaveModal(true)}
title="Save Graph Snapshot"
>
<Focus size={16} /> {focusMode ? 'Show All' : 'Focus Neighborhood'}
<Camera size={16} /> Save Snapshot
</button>
{onSaveSnapshot && (
<button
onClick={() => setShowSaveModal(true)}
title="Save Graph Snapshot"
>
<Camera size={16} /> Save Snapshot
</button>
)}
{hasSelection && (
<div className="selection-info">
Selected: <strong>{selectedName}</strong>
</div>
)}
</div>
)}
{hasSelection && !isMobile && (
<div className="selection-info">
Selected: <strong>{selectedName}</strong>
</div>
)}
</div>
);

return (
<>
{controls}

{showSaveModal && (
<div className="modal-overlay" onClick={() => setShowSaveModal(false)}>
Expand All @@ -86,10 +94,17 @@ const GraphControls: React.FC<GraphControlsProps> = ({
aria-modal="true"
aria-labelledby="modal-title"
>
<h3 id="modal-title"><Camera size={16} /> Save Graph Snapshot</h3>
<p className="modal-meta">
<div className="inspector-header" style={{ marginBottom: 'var(--space-4)', padding: 0, background: 'transparent', border: 0 }}>
<h3 id="modal-title"><Camera size={18} /> Save Graph Snapshot</h3>
<button className="close-button" onClick={() => setShowSaveModal(false)} aria-label="Close modal">
<X size={18} />
</button>
</div>

<p className="modal-meta" style={{ marginBottom: 'var(--space-4)', fontSize: '13px', color: 'var(--text-muted)' }}>
<Clock size={14} /> {new Date().toLocaleString()} | {nodes.length} nodes, {edges.length} edges
</p>

<div className="form-group">
<label htmlFor="snapshot-name">Snapshot Name *</label>
<input
Expand All @@ -109,6 +124,7 @@ const GraphControls: React.FC<GraphControlsProps> = ({
onChange={(e) => setSnapshotDesc(e.target.value)}
placeholder="Optional notes about this snapshot..."
rows={2}
style={{ width: '100%', padding: 'var(--space-2)', borderRadius: 'var(--radius-base)', border: '1px solid var(--border-default)' }}
/>
</div>
<div className="modal-actions">
Expand Down
101 changes: 101 additions & 0 deletions src/features/graph/GraphInspector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useMemo } from 'react';
import { X, ExternalLink, Info, ShieldCheck } from 'lucide-react';
import { Entity, Claim, Link } from '../../lib/validation';

interface GraphInspectorProps {
entity: Entity;
claims: Claim[];
links: Link[];
entities: Entity[];
onClose: () => void;
}

const GraphInspector: React.FC<GraphInspectorProps> = ({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`GraphInspector` has a cyclomatic complexity of 8 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

entity,
claims,
links,
entities,
onClose
}) => {
const outgoingLinks = useMemo(() =>
links.filter(l => l.source_id === entity.id),
[links, entity.id]
);

const incomingLinks = useMemo(() =>
links.filter(l => l.target_id === entity.id),
[links, entity.id]
);

const getEntityName = (id: string) =>
entities.find(e => e.id === id)?.name || 'Unknown Entity';

return (
<aside className="inspector-panel" aria-label={`Details for ${entity.name}`}>
<div className="inspector-header">
<h3>{entity.name}</h3>
<button className="close-button" onClick={onClose} aria-label="Close inspector">
<X size={18} />
</button>
</div>

<div className="inspector-content">
<div className="inspector-section">
<div className="result-type">{entity.type}</div>
{entity.description && (
<p className="result-description">{entity.description}</p>
)}
</div>

{claims.length > 0 && (
<div className="inspector-section">
<h4><ShieldCheck size={14} /> Claims</h4>
<ul className="results-list">
{claims.map(claim => (
<li key={claim.id} className="search-result-item">
<div className="msg-text" style={{ fontSize: '13px' }}>{claim.statement}</div>
{claim.evidence && (
<div className="result-meta" style={{ marginTop: '4px' }}>
<span className="provenance-tag">Evidence: {claim.evidence}</span>
</div>
)}
</li>
))}
</ul>
</div>
)}

{(outgoingLinks.length > 0 || incomingLinks.length > 0) && (
<div className="inspector-section">
<h4><ExternalLink size={14} /> Relationships</h4>
<ul className="results-list">
{outgoingLinks.map(link => (
<li key={link.id} className="search-result-item">
<div style={{ fontSize: '13px' }}>
<strong>{link.relation}</strong> → {getEntityName(link.target_id)}
</div>
</li>
))}
{incomingLinks.map(link => (
<li key={link.id} className="search-result-item">
<div style={{ fontSize: '13px' }}>
{getEntityName(link.source_id)} → <strong>{link.relation}</strong>
</div>
</li>
))}
</ul>
</div>
)}

{claims.length === 0 && outgoingLinks.length === 0 && incomingLinks.length === 0 && (
<div className="no-results-state" style={{ padding: 'var(--space-4)' }}>
<Info size={24} className="no-results-icon" />
<p>No claims or links found for this entity.</p>
</div>
)}
</div>
</aside>
);
};

export default GraphInspector;
Loading
Loading