Skip to content

Commit 00ce92f

Browse files
committed
EasyAI repo scnner and documentation updated
1 parent 03f784c commit 00ce92f

19 files changed

Lines changed: 3525 additions & 4 deletions

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ From simple notes to complex technical documentation, Easyeditor has you covered
1919
* **Export Power**: Export to **PNG**, **TXT**, **PDF**, **MD** and secure **SSTP Encryption**.
2020
* **Customizable**: Choose from beautiful themes or create your own!
2121
* **SSTP Encryption**: SSTP (Simple Security Text Protocol) protection using modern AES-256-CBC encryption!
22+
* **EasyAI Personas**: Built-in AI personas for documentation, diagrams, code fixes, and more — with full repo scanning that reads your project file-by-file to generate accurate, context-aware documentation.
2223

2324
[![Infographic](https://img.shields.io/badge/📊_Infographic-View_PDF-orange?style=for-the-badge)](docs/Easyeditor-Infographic.pdf)
2425

@@ -127,6 +128,15 @@ Seamlessly manage your version control without leaving the editor.
127128

128129
<a><img src="screenshots/git_feature.png" alt="Git Feature" width="720" height="400"></a>
129130

131+
### 🤖 EasyAI Personas
132+
Multiple AI personas for documentation, diagrams, code fixes, user stories, and more. The Documentation persona scans your entire Git repository file-by-file to produce accurate, project-specific documentation.
133+
134+
<a><img src="screenshots/easyai-new-feature-many-personas.png" alt="EasyAI Personas" width="720" height="400"></a>
135+
136+
**Repo Scanning for Documentation:**
137+
138+
<a><img src="screenshots/easyai-scanning-repo-ofr-documentation.png" alt="EasyAI Repo Scanning" width="720" height="400"></a>
139+
130140
### 📝 Table Support
131141
Clean and responsive table rendering.
132142

247 KB
Loading
82.7 KB
Loading

src/App.tsx

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ import EasyNotesSidebar from './components/EasyNotesSidebar';
103103
import EasyAIPanel from './components/EasyAIPanel';
104104
import { buildSystemPrompt, parseFixTarget, extractBlock, extractTable } from './components/easyai/aiPersonas';
105105
import { queryEasyAI } from './components/easyai/aiService';
106+
import { scanRepository } from './components/easyai/repoScanner';
107+
import { generateDocumentation } from './components/easyai/docGenerator';
106108
import FeaturesModal from './components/FeaturesModal';
107109
import ThemeModal from './components/ThemeModal';
108110
import ImportThemeModal from './components/ImportThemeModal';
@@ -256,6 +258,16 @@ const App = () => {
256258
const [pendingCredentialAction, setPendingCredentialAction] = useState<(() => void) | null>(null);
257259
const [prefillCredentials, setPrefillCredentials] = useState<{ username: string; token: string } | null>(null);
258260
const [currentDirHandle, setCurrentDirHandle] = useState<any>(null); // For web File System Access API
261+
262+
// Repo scan progress state
263+
const [scanProgress, setScanProgress] = useState<{
264+
isScanning: boolean;
265+
currentFile: string;
266+
filesProcessed: number;
267+
totalFiles: number;
268+
}>({ isScanning: false, currentFile: '', filesProcessed: 0, totalFiles: 0 });
269+
const scanAbortControllerRef = useRef<AbortController | null>(null);
270+
259271
const [confirmModalConfig, setConfirmModalConfig] = useState<{
260272
open: boolean;
261273
title: string;
@@ -3220,11 +3232,170 @@ const App = () => {
32203232
)
32213233
}
32223234

3235+
{scanProgress.isScanning && (
3236+
<div style={{
3237+
position: 'fixed',
3238+
top: 0,
3239+
left: 0,
3240+
right: 0,
3241+
bottom: 0,
3242+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
3243+
display: 'flex',
3244+
alignItems: 'center',
3245+
justifyContent: 'center',
3246+
zIndex: 10000,
3247+
}}>
3248+
<div style={{
3249+
background: 'var(--bg-color, #1e1e1e)',
3250+
color: 'var(--text-color, #ccc)',
3251+
borderRadius: '8px',
3252+
padding: '24px 32px',
3253+
minWidth: '360px',
3254+
maxWidth: '480px',
3255+
boxShadow: '0 4px 24px rgba(0,0,0,0.4)',
3256+
}}>
3257+
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>Scanning Repository…</h3>
3258+
<div style={{
3259+
background: 'var(--border-color, #333)',
3260+
borderRadius: '4px',
3261+
height: '8px',
3262+
overflow: 'hidden',
3263+
marginBottom: '12px',
3264+
}}>
3265+
<div style={{
3266+
background: 'var(--accent-color, #007acc)',
3267+
height: '100%',
3268+
width: `${scanProgress.totalFiles > 0 ? (scanProgress.filesProcessed / scanProgress.totalFiles) * 100 : 0}%`,
3269+
transition: 'width 0.3s ease',
3270+
borderRadius: '4px',
3271+
}} />
3272+
</div>
3273+
<div style={{ fontSize: '13px', marginBottom: '8px' }}>
3274+
{scanProgress.filesProcessed} / {scanProgress.totalFiles} files
3275+
</div>
3276+
<div style={{
3277+
fontSize: '12px',
3278+
color: 'var(--text-muted, #888)',
3279+
marginBottom: '16px',
3280+
overflow: 'hidden',
3281+
textOverflow: 'ellipsis',
3282+
whiteSpace: 'nowrap',
3283+
}}>
3284+
{scanProgress.currentFile || 'Preparing…'}
3285+
</div>
3286+
<button
3287+
onClick={() => scanAbortControllerRef.current?.abort()}
3288+
style={{
3289+
background: 'var(--danger-color, #d32f2f)',
3290+
color: '#fff',
3291+
border: 'none',
3292+
borderRadius: '4px',
3293+
padding: '6px 18px',
3294+
cursor: 'pointer',
3295+
fontSize: '13px',
3296+
}}
3297+
>
3298+
Cancel
3299+
</button>
3300+
</div>
3301+
</div>
3302+
)}
3303+
32233304
<EasyAIPanel
32243305
showEasyAIPanel={showEasyAIPanel}
32253306
setShowEasyAIPanel={setShowEasyAIPanel}
32263307
showToast={showToast}
32273308
onActionSelect={async (actionId, promptText) => {
3309+
// ── Documentation persona with repo scanning ──
3310+
if (actionId === 'documentation') {
3311+
const isTauri = !!(window as any).__TAURI_INTERNALS__;
3312+
console.log('[EasyAI-Doc] Documentation action triggered');
3313+
console.log('[EasyAI-Doc] isTauri:', isTauri);
3314+
console.log('[EasyAI-Doc] currentRepoPath:', currentRepoPath);
3315+
console.log('[EasyAI-Doc] currentDirHandle:', currentDirHandle);
3316+
console.log('[EasyAI-Doc] isGitRepo state:', isGitRepo);
3317+
3318+
// Tauri uses file paths; web uses FileSystemDirectoryHandle
3319+
const hasTauriRepo = isTauri && currentRepoPath;
3320+
const hasWebRepo = !isTauri && currentDirHandle;
3321+
3322+
if (!hasTauriRepo && !hasWebRepo) {
3323+
console.warn('[EasyAI-Doc] No repository available — aborting');
3324+
showToast('No Git repository loaded. Please open a repository first via EasyGit.', 'warning');
3325+
return;
3326+
}
3327+
3328+
const controller = new AbortController();
3329+
scanAbortControllerRef.current = controller;
3330+
3331+
setScanProgress({ isScanning: true, currentFile: '', filesProcessed: 0, totalFiles: 0 });
3332+
setShowEasyAIPanel(false);
3333+
3334+
try {
3335+
let scanResult;
3336+
3337+
if (hasTauriRepo) {
3338+
console.log('[EasyAI-Doc] Using Tauri scanner for path:', currentRepoPath);
3339+
const { scanRepositoryTauri } = await import('./components/easyai/tauriRepoScanner');
3340+
scanResult = await scanRepositoryTauri({
3341+
repoPath: currentRepoPath!,
3342+
userPrompt: promptText,
3343+
onProgress: (current, total, filePath) => {
3344+
setScanProgress({ isScanning: true, currentFile: filePath, filesProcessed: current, totalFiles: total });
3345+
},
3346+
signal: controller.signal,
3347+
});
3348+
} else {
3349+
console.log('[EasyAI-Doc] Using web scanner with dirHandle:', currentDirHandle.name);
3350+
scanResult = await scanRepository({
3351+
dirHandle: currentDirHandle,
3352+
userPrompt: promptText,
3353+
onProgress: (current, total, filePath) => {
3354+
setScanProgress({ isScanning: true, currentFile: filePath, filesProcessed: current, totalFiles: total });
3355+
},
3356+
signal: controller.signal,
3357+
});
3358+
}
3359+
3360+
if (scanResult.cancelled) {
3361+
showToast('Scan cancelled.', 'info');
3362+
setScanProgress({ isScanning: false, currentFile: '', filesProcessed: 0, totalFiles: 0 });
3363+
return;
3364+
}
3365+
3366+
if (scanResult.cache.size <= 1) {
3367+
showToast('No scannable files found in the repository.', 'warning');
3368+
setScanProgress({ isScanning: false, currentFile: '', filesProcessed: 0, totalFiles: 0 });
3369+
return;
3370+
}
3371+
3372+
// Log scan results for debugging
3373+
console.log(`[RepoScanner] Cache contains ${scanResult.cache.size - 1} file summaries`);
3374+
3375+
setScanProgress(prev => ({ ...prev, currentFile: 'Generating documentation…' }));
3376+
const doc = await generateDocumentation({
3377+
cache: scanResult.cache,
3378+
userPrompt: promptText,
3379+
signal: controller.signal,
3380+
});
3381+
3382+
if (doc) {
3383+
setEditorContent(doc + '\n');
3384+
showToast('EasyAI (documentation) — documentation generated.', 'success');
3385+
} else {
3386+
showToast('EasyAI (documentation) — empty response.', 'warning');
3387+
}
3388+
} catch (err: any) {
3389+
const msg = err.message || 'Scan failed';
3390+
console.error('[EasyAI-Doc] Scan error:', msg, err);
3391+
showToast(msg, 'error');
3392+
} finally {
3393+
setScanProgress({ isScanning: false, currentFile: '', filesProcessed: 0, totalFiles: 0 });
3394+
scanAbortControllerRef.current = null;
3395+
}
3396+
return;
3397+
}
3398+
32283399
const systemPrompt = buildSystemPrompt(actionId, editorContent, promptText);
32293400
if (!systemPrompt) {
32303401
showToast(`Unknown EasyAI action: ${actionId}`, 'error');
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* Property-based tests for CacheStore
3+
* Feature: documentation-persona-repo-scanner, Property 3: CacheStore round-trip
4+
*
5+
* **Validates: Requirements 3.3, 3.4**
6+
*
7+
* Property 3: For any valid SummaryRecord (with non-empty filePath, fileType,
8+
* and summary), adding it to the CacheStore and then retrieving it by filePath
9+
* shall return a record with identical filePath, fileType, and summary values.
10+
*/
11+
12+
import * as fc from 'fast-check';
13+
import { CacheStore, SummaryRecord } from '../cacheStore';
14+
15+
/** Generate a safe path segment (alphanumeric + underscore/hyphen). */
16+
const safeSegment = fc
17+
.array(fc.constantFrom(...'abcdefghijklmnopqrstuvwxyz0123456789_-'.split('')), {
18+
minLength: 1,
19+
maxLength: 10,
20+
})
21+
.map((chars) => chars.join(''));
22+
23+
/** Generate a non-empty relative file path like "src/utils/helper.ts". */
24+
const nonEmptyFilePath = fc
25+
.tuple(
26+
fc.array(safeSegment, { minLength: 1, maxLength: 4 }),
27+
safeSegment,
28+
fc.constantFrom('.ts', '.js', '.md', '.json', '.txt', '.py', '.rs', '.go'),
29+
)
30+
.map(([dirs, name, ext]) => [...dirs, `${name}${ext}`].join('/'));
31+
32+
/** Generate a non-empty file type string. */
33+
const nonEmptyFileType = fc.constantFrom(
34+
'typescript', 'javascript', 'markdown', 'json', 'python', 'rust', 'go', 'text',
35+
);
36+
37+
/** Generate a non-empty summary string. */
38+
const nonEmptySummary = fc
39+
.string({ minLength: 1, maxLength: 200 })
40+
.filter((s) => s.trim().length > 0);
41+
42+
/** Generate a valid SummaryRecord with non-empty fields. */
43+
const summaryRecordArb: fc.Arbitrary<SummaryRecord> = fc
44+
.tuple(nonEmptyFilePath, nonEmptyFileType, nonEmptySummary)
45+
.map(([filePath, fileType, summary]) => ({ filePath, fileType, summary }));
46+
47+
// Feature: documentation-persona-repo-scanner, Property 3: CacheStore round-trip
48+
describe('Property 3: CacheStore round-trip', () => {
49+
// **Validates: Requirements 3.3, 3.4**
50+
// Adding a record and retrieving by filePath returns identical values
51+
it('round-trips any valid SummaryRecord through add and getByPath', () => {
52+
fc.assert(
53+
fc.property(summaryRecordArb, (record) => {
54+
const store = new CacheStore();
55+
store.add(record);
56+
const retrieved = store.getByPath(record.filePath);
57+
expect(retrieved).toBeDefined();
58+
expect(retrieved!.filePath).toBe(record.filePath);
59+
expect(retrieved!.fileType).toBe(record.fileType);
60+
expect(retrieved!.summary).toBe(record.summary);
61+
}),
62+
{ numRuns: 100 },
63+
);
64+
});
65+
66+
// **Validates: Requirements 3.3, 3.4**
67+
// Adding multiple distinct records and retrieving each returns the correct record
68+
it('round-trips multiple distinct SummaryRecords', () => {
69+
fc.assert(
70+
fc.property(
71+
fc.array(summaryRecordArb, { minLength: 1, maxLength: 20 }),
72+
(records) => {
73+
const store = new CacheStore();
74+
// Deduplicate by filePath — last one wins (matches overwrite semantics)
75+
const expected = new Map<string, SummaryRecord>();
76+
for (const r of records) {
77+
store.add(r);
78+
expected.set(r.filePath, r);
79+
}
80+
81+
expect(store.size).toBe(expected.size);
82+
83+
for (const [path, rec] of expected) {
84+
const retrieved = store.getByPath(path);
85+
expect(retrieved).toBeDefined();
86+
expect(retrieved!.filePath).toBe(rec.filePath);
87+
expect(retrieved!.fileType).toBe(rec.fileType);
88+
expect(retrieved!.summary).toBe(rec.summary);
89+
}
90+
},
91+
),
92+
{ numRuns: 100 },
93+
);
94+
});
95+
96+
// **Validates: Requirements 3.3, 3.4**
97+
// Duplicate filePath additions overwrite the previous record
98+
it('overwrites previous record when adding duplicate filePath', () => {
99+
fc.assert(
100+
fc.property(
101+
nonEmptyFilePath,
102+
nonEmptyFileType,
103+
nonEmptySummary,
104+
nonEmptyFileType,
105+
nonEmptySummary,
106+
(filePath, type1, summary1, type2, summary2) => {
107+
const store = new CacheStore();
108+
store.add({ filePath, fileType: type1, summary: summary1 });
109+
store.add({ filePath, fileType: type2, summary: summary2 });
110+
111+
expect(store.size).toBe(1);
112+
const retrieved = store.getByPath(filePath);
113+
expect(retrieved).toBeDefined();
114+
expect(retrieved!.fileType).toBe(type2);
115+
expect(retrieved!.summary).toBe(summary2);
116+
},
117+
),
118+
{ numRuns: 100 },
119+
);
120+
});
121+
122+
// **Validates: Requirements 3.3, 3.4**
123+
// getAll returns all added records and they match what was added
124+
it('getAll returns all stored records with correct values', () => {
125+
fc.assert(
126+
fc.property(
127+
fc.array(summaryRecordArb, { minLength: 1, maxLength: 20 }),
128+
(records) => {
129+
const store = new CacheStore();
130+
const expected = new Map<string, SummaryRecord>();
131+
for (const r of records) {
132+
store.add(r);
133+
expected.set(r.filePath, r);
134+
}
135+
136+
const all = store.getAll();
137+
expect(all.length).toBe(expected.size);
138+
139+
for (const rec of all) {
140+
const exp = expected.get(rec.filePath);
141+
expect(exp).toBeDefined();
142+
expect(rec.filePath).toBe(exp!.filePath);
143+
expect(rec.fileType).toBe(exp!.fileType);
144+
expect(rec.summary).toBe(exp!.summary);
145+
}
146+
},
147+
),
148+
{ numRuns: 100 },
149+
);
150+
});
151+
});

0 commit comments

Comments
 (0)