@feltdb/core 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-feltdb.js +0 -0
- package/dist/create/cli.js +51 -2
- package/dist/create/create.js +1349 -125
- package/dist/create/package-versions.js +1 -1
- package/dist/studio/KeyManagementPanel-B0s0xAXz.js +298 -0
- package/dist/studio/components/KeyManagementPanel.d.ts.map +1 -1
- package/dist/studio/components/KeyManagementPanel.js +1 -1
- package/dist/studio/components/index.js +2 -2
- package/dist/studio/{components-BRYUceo9.js → components-BAycgZhP.js} +1 -1
- package/dist/studio/index.js +2 -2
- package/dist/studio-app/assets/{feltdb_wasm-BXMn9UxO.js → feltdb_wasm-Bb1Pg6qz.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-2_wVudcZ.wasm +0 -0
- package/dist/studio-app/assets/{index-B-lZjdtI.js → index-DKVLtS37.js} +2 -2
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/dist/studio/KeyManagementPanel-BOvWTRyH.js +0 -246
- package/dist/studio-app/assets/feltdb_wasm_bg-bYYbZeRM.wasm +0 -0
package/dist/create/create.js
CHANGED
|
@@ -180,12 +180,16 @@ import { db, reports } from '../../src/feltdb';
|
|
|
180
180
|
export const researcher = db.defineAgent({
|
|
181
181
|
name: 'researcher',
|
|
182
182
|
version: 1,
|
|
183
|
-
description: '
|
|
183
|
+
description: 'Private, on-device AI assistant for document enhancement and analysis',
|
|
184
184
|
capabilities: [
|
|
185
185
|
'document-read',
|
|
186
186
|
'report-write'
|
|
187
187
|
],
|
|
188
|
-
goals: [
|
|
188
|
+
goals: [
|
|
189
|
+
'Analyze documents privately without sending data off-device',
|
|
190
|
+
'Generate insights and enhancements locally',
|
|
191
|
+
'Support document summarization, expansion, and improvement'
|
|
192
|
+
],
|
|
189
193
|
constraints: { maxIterations: 1 },
|
|
190
194
|
});
|
|
191
195
|
|
|
@@ -195,20 +199,138 @@ export async function runResearcher(
|
|
|
195
199
|
prompt: string,
|
|
196
200
|
onProgress?: (message: string) => void,
|
|
197
201
|
) {
|
|
198
|
-
provider
|
|
199
|
-
|
|
200
|
-
onProgress
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
202
|
+
if (!provider) {
|
|
203
|
+
provider = new WebLLMProvider({
|
|
204
|
+
onProgress: ({ progress, text }) => {
|
|
205
|
+
const pct = Math.round(progress * 100);
|
|
206
|
+
onProgress?.(\`\${pct}% - \${text}\`);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const systemPrompt = \`You are a helpful AI assistant that operates entirely in the browser.
|
|
213
|
+
You help users:
|
|
214
|
+
- Summarize documents concisely
|
|
215
|
+
- Expand and enhance text with more details
|
|
216
|
+
- Generate ideas and suggestions
|
|
217
|
+
- Improve writing clarity and structure
|
|
218
|
+
- Answer questions about document content
|
|
219
|
+
|
|
220
|
+
Always be concise and practical. Respect the user's intent.\`;
|
|
221
|
+
|
|
222
|
+
const content = await provider.generate([
|
|
223
|
+
{ role: 'system', content: systemPrompt },
|
|
224
|
+
{ role: 'user', content: prompt },
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
await reports.insert({
|
|
228
|
+
id: 'report_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
|
|
229
|
+
title: prompt.slice(0, 80) || 'AI Research Result',
|
|
230
|
+
content,
|
|
231
|
+
createdAt: new Date().toISOString(),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return content;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to generate response');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function summarizeDocument(
|
|
241
|
+
title: string,
|
|
242
|
+
content: string,
|
|
243
|
+
onProgress?: (message: string) => void,
|
|
244
|
+
): Promise<string> {
|
|
245
|
+
const prompt = \`Summarize this document in 2-3 concise sentences:
|
|
246
|
+
|
|
247
|
+
Title: \${title}
|
|
248
|
+
|
|
249
|
+
Content: \${content.substring(0, 2000)}\`;
|
|
250
|
+
|
|
251
|
+
return runResearcher(prompt, onProgress);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function enhanceDocument(
|
|
255
|
+
title: string,
|
|
256
|
+
content: string,
|
|
257
|
+
onProgress?: (message: string) => void,
|
|
258
|
+
): Promise<string> {
|
|
259
|
+
const prompt = \`Enhance and improve this document by making it more detailed and clearer:
|
|
260
|
+
|
|
261
|
+
Title: \${title}
|
|
262
|
+
|
|
263
|
+
Content: \${content}
|
|
264
|
+
|
|
265
|
+
Provide the enhanced version:\`;
|
|
266
|
+
|
|
267
|
+
return runResearcher(prompt, onProgress);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function generateIdeas(
|
|
271
|
+
topic: string,
|
|
272
|
+
onProgress?: (message: string) => void,
|
|
273
|
+
): Promise<string> {
|
|
274
|
+
const prompt = \`Generate 5 creative ideas related to: \${topic}
|
|
275
|
+
|
|
276
|
+
Format as a numbered list with brief explanations.\`;
|
|
277
|
+
|
|
278
|
+
return runResearcher(prompt, onProgress);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function generateFeltDBFeature(
|
|
282
|
+
featureDescription: string,
|
|
283
|
+
onProgress?: (message: string) => void,
|
|
284
|
+
): Promise<string> {
|
|
285
|
+
const prompt = \`Generate FeltDB flow syntax for: \${featureDescription}
|
|
286
|
+
|
|
287
|
+
Include:
|
|
288
|
+
1. Collection definitions with proper types
|
|
289
|
+
2. Capability definitions
|
|
290
|
+
3. Workflow definitions if needed
|
|
291
|
+
4. Policy rules for access control
|
|
292
|
+
|
|
293
|
+
Format the output as valid FeltDB flow language.\`;
|
|
294
|
+
|
|
295
|
+
return runResearcher(prompt, onProgress);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function generateReactComponent(
|
|
299
|
+
componentDescription: string,
|
|
300
|
+
onProgress?: (message: string) => void,
|
|
301
|
+
): Promise<string> {
|
|
302
|
+
const prompt = \`Generate a React component for: \${componentDescription}
|
|
303
|
+
|
|
304
|
+
Include:
|
|
305
|
+
1. TypeScript types/interfaces
|
|
306
|
+
2. useState and useEffect hooks
|
|
307
|
+
3. Error handling
|
|
308
|
+
4. Loading states
|
|
309
|
+
5. Proper styling with CSS classes
|
|
310
|
+
|
|
311
|
+
Export a functional component that can be imported and used.\`;
|
|
312
|
+
|
|
313
|
+
return runResearcher(prompt, onProgress);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export async function generateCapability(
|
|
317
|
+
capabilityName: string,
|
|
318
|
+
description: string,
|
|
319
|
+
onProgress?: (message: string) => void,
|
|
320
|
+
): Promise<string> {
|
|
321
|
+
const prompt = \`Generate a FeltDB capability definition for: \${capabilityName}
|
|
322
|
+
|
|
323
|
+
Description: \${description}
|
|
324
|
+
|
|
325
|
+
Include:
|
|
326
|
+
1. TypeScript interface definitions
|
|
327
|
+
2. Implementation with proper types
|
|
328
|
+
3. Error handling
|
|
329
|
+
4. Documentation comments
|
|
330
|
+
|
|
331
|
+
Make it production-ready.\`;
|
|
332
|
+
|
|
333
|
+
return runResearcher(prompt, onProgress);
|
|
212
334
|
}
|
|
213
335
|
`;
|
|
214
336
|
fs.writeFileSync(path.join(feltdbDir, 'agents', 'researcher.ts'), agentTs);
|
|
@@ -240,24 +362,44 @@ export const capabilities = {
|
|
|
240
362
|
// Create main application file based on framework
|
|
241
363
|
if (framework === 'react') {
|
|
242
364
|
const agentImport = hasAgents
|
|
243
|
-
? "import { runResearcher } from '../feltdb/agents/researcher';"
|
|
365
|
+
? "import { runResearcher, summarizeDocument, enhanceDocument, generateIdeas, generateFeltDBFeature, generateReactComponent, generateCapability } from '../feltdb/agents/researcher';"
|
|
244
366
|
: '';
|
|
245
367
|
const agentState = hasAgents
|
|
246
|
-
? ` const [prompt, setPrompt] = useState('
|
|
368
|
+
? ` const [prompt, setPrompt] = useState('');
|
|
247
369
|
const [answer, setAnswer] = useState('');
|
|
248
370
|
const [modelStatus, setModelStatus] = useState('Model not loaded');
|
|
249
|
-
const [generating, setGenerating] = useState(false)
|
|
371
|
+
const [generating, setGenerating] = useState(false);
|
|
372
|
+
const [selectedDoc, setSelectedDoc] = useState<any | null>(null);
|
|
373
|
+
const [aiMode, setAiMode] = useState<'custom' | 'summarize' | 'enhance' | 'ideas' | 'feltdb' | 'react' | 'capability'>('custom');`
|
|
250
374
|
: '';
|
|
251
375
|
const agentHandler = hasAgents
|
|
252
376
|
? `
|
|
253
|
-
const
|
|
377
|
+
const handleRunAI = async (mode: 'custom' | 'summarize' | 'enhance' | 'ideas' | 'feltdb' | 'react' | 'capability') => {
|
|
254
378
|
setGenerating(true);
|
|
255
379
|
setAnswer('');
|
|
380
|
+
setModelStatus('Running inference...');
|
|
381
|
+
|
|
256
382
|
try {
|
|
257
|
-
|
|
258
|
-
|
|
383
|
+
let result = '';
|
|
384
|
+
if (mode === 'custom') {
|
|
385
|
+
result = await runResearcher(prompt, setModelStatus);
|
|
386
|
+
} else if (mode === 'summarize' && selectedDoc) {
|
|
387
|
+
result = await summarizeDocument(selectedDoc.title, selectedDoc.content, setModelStatus);
|
|
388
|
+
} else if (mode === 'enhance' && selectedDoc) {
|
|
389
|
+
result = await enhanceDocument(selectedDoc.title, selectedDoc.content, setModelStatus);
|
|
390
|
+
} else if (mode === 'ideas') {
|
|
391
|
+
result = await generateIdeas(prompt, setModelStatus);
|
|
392
|
+
} else if (mode === 'feltdb') {
|
|
393
|
+
result = await generateFeltDBFeature(prompt, setModelStatus);
|
|
394
|
+
} else if (mode === 'react') {
|
|
395
|
+
result = await generateReactComponent(prompt, setModelStatus);
|
|
396
|
+
} else if (mode === 'capability') {
|
|
397
|
+
result = await generateCapability('Feature', prompt, setModelStatus);
|
|
398
|
+
}
|
|
399
|
+
setAnswer(result);
|
|
400
|
+
setModelStatus('✓ Ready — all processing done locally');
|
|
259
401
|
} catch (error) {
|
|
260
|
-
setModelStatus(error instanceof Error ? error.message : String(error));
|
|
402
|
+
setModelStatus('✗ Error: ' + (error instanceof Error ? error.message : String(error)));
|
|
261
403
|
} finally {
|
|
262
404
|
setGenerating(false);
|
|
263
405
|
}
|
|
@@ -267,13 +409,167 @@ export const capabilities = {
|
|
|
267
409
|
const agentMarkup = hasAgents
|
|
268
410
|
? `
|
|
269
411
|
<section className="researcher">
|
|
270
|
-
<h2
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
<
|
|
274
|
-
|
|
412
|
+
<h2>🤖 AI Assistant (Private, Local)</h2>
|
|
413
|
+
<div className="status-badge">{modelStatus}</div>
|
|
414
|
+
|
|
415
|
+
<div className="ai-modes">
|
|
416
|
+
<div className="mode-group">
|
|
417
|
+
<span className="mode-group-label">📝 Content</span>
|
|
418
|
+
<button
|
|
419
|
+
className={\`mode-btn \${aiMode === 'custom' ? 'active' : ''}\`}
|
|
420
|
+
onClick={() => setAiMode('custom')}
|
|
421
|
+
>
|
|
422
|
+
✍️ Custom
|
|
423
|
+
</button>
|
|
424
|
+
<button
|
|
425
|
+
className={\`mode-btn \${aiMode === 'summarize' ? 'active' : ''}\`}
|
|
426
|
+
onClick={() => setAiMode('summarize')}
|
|
427
|
+
disabled={!selectedDoc}
|
|
428
|
+
>
|
|
429
|
+
📄 Summarize
|
|
430
|
+
</button>
|
|
431
|
+
<button
|
|
432
|
+
className={\`mode-btn \${aiMode === 'enhance' ? 'active' : ''}\`}
|
|
433
|
+
onClick={() => setAiMode('enhance')}
|
|
434
|
+
disabled={!selectedDoc}
|
|
435
|
+
>
|
|
436
|
+
✨ Enhance
|
|
437
|
+
</button>
|
|
438
|
+
<button
|
|
439
|
+
className={\`mode-btn \${aiMode === 'ideas' ? 'active' : ''}\`}
|
|
440
|
+
onClick={() => setAiMode('ideas')}
|
|
441
|
+
>
|
|
442
|
+
💡 Ideas
|
|
443
|
+
</button>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<div className="mode-group">
|
|
447
|
+
<span className="mode-group-label">💻 Code Gen</span>
|
|
448
|
+
<button
|
|
449
|
+
className={\`mode-btn code-mode \${aiMode === 'feltdb' ? 'active' : ''}\`}
|
|
450
|
+
onClick={() => setAiMode('feltdb')}
|
|
451
|
+
>
|
|
452
|
+
⚙️ FeltDB Flow
|
|
453
|
+
</button>
|
|
454
|
+
<button
|
|
455
|
+
className={\`mode-btn code-mode \${aiMode === 'react' ? 'active' : ''}\`}
|
|
456
|
+
onClick={() => setAiMode('react')}
|
|
457
|
+
>
|
|
458
|
+
⚛️ React
|
|
459
|
+
</button>
|
|
460
|
+
<button
|
|
461
|
+
className={\`mode-btn code-mode \${aiMode === 'capability' ? 'active' : ''}\`}
|
|
462
|
+
onClick={() => setAiMode('capability')}
|
|
463
|
+
>
|
|
464
|
+
🔧 Capability
|
|
465
|
+
</button>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
{aiMode === 'summarize' || aiMode === 'enhance' ? (
|
|
470
|
+
<div className="doc-selector">
|
|
471
|
+
<label>Select a document:</label>
|
|
472
|
+
<select
|
|
473
|
+
value={selectedDoc?.id || ''}
|
|
474
|
+
onChange={(e) => {
|
|
475
|
+
const doc = docs.find((d: any) => d.id === e.target.value);
|
|
476
|
+
setSelectedDoc(doc || null);
|
|
477
|
+
}}
|
|
478
|
+
>
|
|
479
|
+
<option value="">Choose a document...</option>
|
|
480
|
+
{docs.map((doc: any) => (
|
|
481
|
+
<option key={doc.id} value={doc.id}>
|
|
482
|
+
{doc.title}
|
|
483
|
+
</option>
|
|
484
|
+
))}
|
|
485
|
+
</select>
|
|
486
|
+
</div>
|
|
487
|
+
) : (
|
|
488
|
+
<textarea
|
|
489
|
+
value={prompt}
|
|
490
|
+
onChange={(event) => setPrompt(event.target.value)}
|
|
491
|
+
placeholder={
|
|
492
|
+
aiMode === 'ideas' ? 'What topic do you want ideas for?' :
|
|
493
|
+
aiMode === 'feltdb' ? 'Describe the FeltDB feature you want to create...' :
|
|
494
|
+
aiMode === 'react' ? 'Describe the React component you need...' :
|
|
495
|
+
aiMode === 'capability' ? 'Describe the capability implementation...' :
|
|
496
|
+
'Enter your prompt...'
|
|
497
|
+
}
|
|
498
|
+
rows={5}
|
|
499
|
+
/>
|
|
500
|
+
)}
|
|
501
|
+
|
|
502
|
+
<button
|
|
503
|
+
onClick={() => handleRunAI(aiMode)}
|
|
504
|
+
disabled={
|
|
505
|
+
generating ||
|
|
506
|
+
(aiMode === 'custom' && !prompt.trim()) ||
|
|
507
|
+
((aiMode === 'summarize' || aiMode === 'enhance') && !selectedDoc) ||
|
|
508
|
+
((aiMode === 'ideas' || aiMode === 'feltdb' || aiMode === 'react' || aiMode === 'capability') && !prompt.trim())
|
|
509
|
+
}
|
|
510
|
+
className="research-btn"
|
|
511
|
+
>
|
|
512
|
+
{generating ? '⏳ Running locally…' : '🚀 Generate'}
|
|
275
513
|
</button>
|
|
276
|
-
|
|
514
|
+
|
|
515
|
+
{answer && (
|
|
516
|
+
<article className="research-result">
|
|
517
|
+
<h3>Generated Output</h3>
|
|
518
|
+
<div className="result-content">{answer}</div>
|
|
519
|
+
<div className="result-actions">
|
|
520
|
+
{aiMode === 'custom' || aiMode === 'enhance' || aiMode === 'ideas' ? (
|
|
521
|
+
<button
|
|
522
|
+
onClick={() => {
|
|
523
|
+
const title = aiMode === 'ideas' ? prompt : (selectedDoc?.title || 'AI Generated');
|
|
524
|
+
documents.insert({
|
|
525
|
+
id: 'doc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
|
|
526
|
+
title: title + ' (AI Enhanced)',
|
|
527
|
+
content: answer,
|
|
528
|
+
createdAt: new Date().toISOString(),
|
|
529
|
+
});
|
|
530
|
+
loadDocs();
|
|
531
|
+
setAnswer('');
|
|
532
|
+
}}
|
|
533
|
+
className="save-result-btn"
|
|
534
|
+
>
|
|
535
|
+
💾 Save to Documents
|
|
536
|
+
</button>
|
|
537
|
+
) : (
|
|
538
|
+
<>
|
|
539
|
+
<button
|
|
540
|
+
onClick={() => {
|
|
541
|
+
const filename = aiMode === 'feltdb' ? 'feature.flow' :
|
|
542
|
+
aiMode === 'react' ? 'Component.tsx' :
|
|
543
|
+
'capability.ts';
|
|
544
|
+
const blob = new Blob([answer], { type: 'text/plain' });
|
|
545
|
+
const url = URL.createObjectURL(blob);
|
|
546
|
+
const a = document.createElement('a');
|
|
547
|
+
a.href = url;
|
|
548
|
+
a.download = filename;
|
|
549
|
+
a.click();
|
|
550
|
+
URL.revokeObjectURL(url);
|
|
551
|
+
}}
|
|
552
|
+
className="save-result-btn"
|
|
553
|
+
>
|
|
554
|
+
📥 Download Code
|
|
555
|
+
</button>
|
|
556
|
+
<button
|
|
557
|
+
onClick={() => {
|
|
558
|
+
navigator.clipboard.writeText(answer);
|
|
559
|
+
alert('Code copied to clipboard!');
|
|
560
|
+
}}
|
|
561
|
+
className="copy-result-btn"
|
|
562
|
+
>
|
|
563
|
+
📋 Copy Code
|
|
564
|
+
</button>
|
|
565
|
+
</>
|
|
566
|
+
)}
|
|
567
|
+
</div>
|
|
568
|
+
<p className="code-note">
|
|
569
|
+
💡 Tip: Generated code can be downloaded, copied, or saved to documents for later reference.
|
|
570
|
+
</p>
|
|
571
|
+
</article>
|
|
572
|
+
)}
|
|
277
573
|
</section>`
|
|
278
574
|
: '';
|
|
279
575
|
const appTsx = `import React, { useState, useEffect } from 'react';
|
|
@@ -283,35 +579,109 @@ ${agentImport}
|
|
|
283
579
|
export function App() {
|
|
284
580
|
const [docs, setDocs] = useState<any[]>([]);
|
|
285
581
|
const [loading, setLoading] = useState(true);
|
|
582
|
+
const [error, setError] = useState<string | null>(null);
|
|
583
|
+
const [addingDoc, setAddingDoc] = useState(false);
|
|
584
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
585
|
+
const [editingId, setEditingId] = useState<string | null>(null);
|
|
586
|
+
const [editTitle, setEditTitle] = useState('');
|
|
587
|
+
const [editContent, setEditContent] = useState('');
|
|
588
|
+
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
286
589
|
${agentState}
|
|
287
590
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
591
|
+
const loadDocs = async () => {
|
|
592
|
+
try {
|
|
593
|
+
setError(null);
|
|
594
|
+
const allDocs = await documents.find({});
|
|
595
|
+
setDocs(allDocs.sort((a: any, b: any) =>
|
|
596
|
+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
597
|
+
));
|
|
598
|
+
} catch (err) {
|
|
599
|
+
const msg = err instanceof Error ? err.message : 'Failed to load documents';
|
|
600
|
+
console.error('Error loading documents:', err);
|
|
601
|
+
setError(msg);
|
|
602
|
+
} finally {
|
|
603
|
+
setLoading(false);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
299
606
|
|
|
607
|
+
useEffect(() => {
|
|
300
608
|
loadDocs();
|
|
301
609
|
}, []);
|
|
302
610
|
|
|
611
|
+
const filteredDocs = docs.filter((doc: any) =>
|
|
612
|
+
doc.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
613
|
+
doc.content.toLowerCase().includes(searchTerm.toLowerCase())
|
|
614
|
+
);
|
|
615
|
+
|
|
303
616
|
const handleAddDocument = async () => {
|
|
617
|
+
if (addingDoc) return;
|
|
618
|
+
setAddingDoc(true);
|
|
619
|
+
setError(null);
|
|
620
|
+
|
|
304
621
|
try {
|
|
305
|
-
|
|
622
|
+
const newDoc = {
|
|
623
|
+
id: 'doc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
|
|
306
624
|
title: 'New Document',
|
|
307
|
-
content: 'Enter content here',
|
|
308
|
-
createdAt: new Date(),
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
setDocs(allDocs);
|
|
625
|
+
content: 'Enter your content here...',
|
|
626
|
+
createdAt: new Date().toISOString(),
|
|
627
|
+
};
|
|
628
|
+
await documents.insert(newDoc);
|
|
629
|
+
await loadDocs();
|
|
313
630
|
} catch (err) {
|
|
631
|
+
const msg = err instanceof Error ? err.message : 'Failed to add document';
|
|
314
632
|
console.error('Error adding document:', err);
|
|
633
|
+
setError(msg);
|
|
634
|
+
} finally {
|
|
635
|
+
setAddingDoc(false);
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const handleEditStart = (doc: any) => {
|
|
640
|
+
setEditingId(doc.id);
|
|
641
|
+
setEditTitle(doc.title);
|
|
642
|
+
setEditContent(doc.content);
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const handleEditSave = async () => {
|
|
646
|
+
if (!editingId) return;
|
|
647
|
+
setError(null);
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
const doc = docs.find((d: any) => d.id === editingId);
|
|
651
|
+
if (doc) {
|
|
652
|
+
const updatedDoc = {
|
|
653
|
+
...doc,
|
|
654
|
+
title: editTitle,
|
|
655
|
+
content: editContent,
|
|
656
|
+
updatedAt: new Date().toISOString(),
|
|
657
|
+
};
|
|
658
|
+
await documents.insert(updatedDoc);
|
|
659
|
+
setEditingId(null);
|
|
660
|
+
await loadDocs();
|
|
661
|
+
}
|
|
662
|
+
} catch (err) {
|
|
663
|
+
const msg = err instanceof Error ? err.message : 'Failed to save document';
|
|
664
|
+
console.error('Error saving document:', err);
|
|
665
|
+
setError(msg);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const handleDelete = async (docId: string) => {
|
|
670
|
+
if (deletingId) return;
|
|
671
|
+
setDeletingId(docId);
|
|
672
|
+
setError(null);
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
const doc = docs.find((d: any) => d.id === docId);
|
|
676
|
+
if (doc) {
|
|
677
|
+
await documents.delete(doc);
|
|
678
|
+
await loadDocs();
|
|
679
|
+
}
|
|
680
|
+
} catch (err) {
|
|
681
|
+
const msg = err instanceof Error ? err.message : 'Failed to delete document';
|
|
682
|
+
setError(msg);
|
|
683
|
+
} finally {
|
|
684
|
+
setDeletingId(null);
|
|
315
685
|
}
|
|
316
686
|
};
|
|
317
687
|
${agentHandler}
|
|
@@ -339,37 +709,123 @@ ${agentHandler}
|
|
|
339
709
|
</div>
|
|
340
710
|
</section>
|
|
341
711
|
|
|
712
|
+
{error && (
|
|
713
|
+
<section className="error-message">
|
|
714
|
+
<strong>⚠ Error:</strong> {error}
|
|
715
|
+
<button onClick={() => window.location.reload()} className="retry-btn">
|
|
716
|
+
Retry
|
|
717
|
+
</button>
|
|
718
|
+
</section>
|
|
719
|
+
)}
|
|
720
|
+
|
|
342
721
|
<section className="documents">
|
|
343
|
-
<
|
|
722
|
+
<div className="docs-header">
|
|
723
|
+
<h2>📚 Documents</h2>
|
|
724
|
+
<div className="docs-controls">
|
|
725
|
+
<input
|
|
726
|
+
type="text"
|
|
727
|
+
placeholder="🔍 Search documents..."
|
|
728
|
+
value={searchTerm}
|
|
729
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
730
|
+
className="search-input"
|
|
731
|
+
/>
|
|
732
|
+
<button
|
|
733
|
+
onClick={handleAddDocument}
|
|
734
|
+
disabled={addingDoc || loading}
|
|
735
|
+
className="primary-btn"
|
|
736
|
+
>
|
|
737
|
+
{addingDoc ? '⏳ Adding...' : '➕ New Document'}
|
|
738
|
+
</button>
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
|
|
344
742
|
{loading ? (
|
|
345
|
-
<
|
|
743
|
+
<div className="loading">
|
|
744
|
+
<div className="spinner"></div>
|
|
745
|
+
<p>Loading documents...</p>
|
|
746
|
+
</div>
|
|
346
747
|
) : docs.length === 0 ? (
|
|
347
|
-
<
|
|
748
|
+
<div className="empty-state">
|
|
749
|
+
<p>📭 No documents yet.</p>
|
|
750
|
+
<p className="hint">Create one to get started!</p>
|
|
751
|
+
</div>
|
|
752
|
+
) : filteredDocs.length === 0 ? (
|
|
753
|
+
<div className="empty-state">
|
|
754
|
+
<p>🔍 No documents match "{searchTerm}"</p>
|
|
755
|
+
</div>
|
|
348
756
|
) : (
|
|
349
|
-
<
|
|
350
|
-
{
|
|
351
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
757
|
+
<div className="docs-grid">
|
|
758
|
+
{filteredDocs.map((doc: any) => (
|
|
759
|
+
<div key={doc.id} className="doc-card">
|
|
760
|
+
{editingId === doc.id ? (
|
|
761
|
+
<div className="edit-mode">
|
|
762
|
+
<input
|
|
763
|
+
type="text"
|
|
764
|
+
value={editTitle}
|
|
765
|
+
onChange={(e) => setEditTitle(e.target.value)}
|
|
766
|
+
className="edit-title"
|
|
767
|
+
/>
|
|
768
|
+
<textarea
|
|
769
|
+
value={editContent}
|
|
770
|
+
onChange={(e) => setEditContent(e.target.value)}
|
|
771
|
+
className="edit-content"
|
|
772
|
+
rows={6}
|
|
773
|
+
/>
|
|
774
|
+
<div className="edit-actions">
|
|
775
|
+
<button onClick={handleEditSave} className="save-btn">
|
|
776
|
+
✓ Save
|
|
777
|
+
</button>
|
|
778
|
+
<button onClick={() => setEditingId(null)} className="cancel-btn">
|
|
779
|
+
✕ Cancel
|
|
780
|
+
</button>
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
) : (
|
|
784
|
+
<>
|
|
785
|
+
<h3>{doc.title}</h3>
|
|
786
|
+
<p className="doc-preview">{doc.content.substring(0, 150)}...</p>
|
|
787
|
+
<div className="doc-meta">
|
|
788
|
+
<small>{new Date(doc.createdAt).toLocaleDateString()}</small>
|
|
789
|
+
<span className="doc-size">{doc.content.length} chars</span>
|
|
790
|
+
</div>
|
|
791
|
+
<div className="doc-actions">
|
|
792
|
+
<button
|
|
793
|
+
onClick={() => handleEditStart(doc)}
|
|
794
|
+
className="edit-btn"
|
|
795
|
+
>
|
|
796
|
+
✎ Edit
|
|
797
|
+
</button>
|
|
798
|
+
<button
|
|
799
|
+
onClick={() => handleDelete(doc.id)}
|
|
800
|
+
disabled={deletingId === doc.id}
|
|
801
|
+
className="delete-btn"
|
|
802
|
+
>
|
|
803
|
+
{deletingId === doc.id ? '⏳ Deleting...' : '🗑 Delete'}
|
|
804
|
+
</button>
|
|
805
|
+
</div>
|
|
806
|
+
</>
|
|
807
|
+
)}
|
|
808
|
+
</div>
|
|
355
809
|
))}
|
|
356
|
-
</
|
|
810
|
+
</div>
|
|
357
811
|
)}
|
|
358
|
-
<button onClick={handleAddDocument}>Add Document</button>
|
|
359
812
|
</section>
|
|
360
813
|
${agentMarkup}
|
|
361
814
|
</main>
|
|
362
815
|
|
|
363
816
|
<style>{\`
|
|
817
|
+
* { box-sizing: border-box; }
|
|
818
|
+
|
|
364
819
|
body {
|
|
365
820
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
|
|
366
821
|
margin: 0;
|
|
367
822
|
padding: 0;
|
|
368
|
-
background: #
|
|
823
|
+
background: #f5f7fa;
|
|
824
|
+
color: #333;
|
|
369
825
|
}
|
|
370
826
|
|
|
371
827
|
.app {
|
|
372
|
-
max-width:
|
|
828
|
+
max-width: 1400px;
|
|
373
829
|
margin: 0 auto;
|
|
374
830
|
padding: 20px;
|
|
375
831
|
}
|
|
@@ -377,112 +833,549 @@ ${agentMarkup}
|
|
|
377
833
|
header {
|
|
378
834
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
379
835
|
color: white;
|
|
380
|
-
padding: 30px;
|
|
381
|
-
border-radius:
|
|
836
|
+
padding: 40px 30px;
|
|
837
|
+
border-radius: 12px;
|
|
382
838
|
margin-bottom: 30px;
|
|
839
|
+
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
|
|
383
840
|
}
|
|
384
841
|
|
|
385
842
|
header h1 {
|
|
386
843
|
margin: 0 0 10px 0;
|
|
387
|
-
font-size:
|
|
844
|
+
font-size: 32px;
|
|
845
|
+
font-weight: 700;
|
|
388
846
|
}
|
|
389
847
|
|
|
390
848
|
header p {
|
|
391
849
|
margin: 0;
|
|
392
|
-
opacity: 0.
|
|
850
|
+
opacity: 0.95;
|
|
851
|
+
font-size: 16px;
|
|
393
852
|
}
|
|
394
853
|
|
|
395
854
|
main {
|
|
396
855
|
background: white;
|
|
397
|
-
padding:
|
|
398
|
-
border-radius:
|
|
399
|
-
box-shadow: 0 2px
|
|
856
|
+
padding: 30px;
|
|
857
|
+
border-radius: 12px;
|
|
858
|
+
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
400
859
|
}
|
|
401
860
|
|
|
402
861
|
.stats {
|
|
403
862
|
display: grid;
|
|
404
|
-
grid-template-columns: repeat(auto-fit, minmax(
|
|
863
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
405
864
|
gap: 20px;
|
|
406
865
|
margin-bottom: 30px;
|
|
407
866
|
}
|
|
408
867
|
|
|
409
868
|
.stat {
|
|
410
|
-
padding:
|
|
411
|
-
background: #f9f9f9;
|
|
412
|
-
border-radius:
|
|
413
|
-
border-left:
|
|
869
|
+
padding: 24px;
|
|
870
|
+
background: linear-gradient(135deg, #f5f7fa 0%, #f9f9f9 100%);
|
|
871
|
+
border-radius: 8px;
|
|
872
|
+
border-left: 4px solid #667eea;
|
|
873
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
414
874
|
}
|
|
415
875
|
|
|
416
876
|
.stat .label {
|
|
417
877
|
display: block;
|
|
418
|
-
color: #
|
|
419
|
-
font-size:
|
|
878
|
+
color: #999;
|
|
879
|
+
font-size: 10px;
|
|
420
880
|
text-transform: uppercase;
|
|
421
|
-
|
|
881
|
+
letter-spacing: 1px;
|
|
882
|
+
margin-bottom: 8px;
|
|
883
|
+
font-weight: 700;
|
|
422
884
|
}
|
|
423
885
|
|
|
424
886
|
.stat .value {
|
|
425
887
|
display: block;
|
|
426
|
-
font-size:
|
|
427
|
-
font-weight:
|
|
428
|
-
color: #
|
|
888
|
+
font-size: 32px;
|
|
889
|
+
font-weight: 700;
|
|
890
|
+
color: #667eea;
|
|
429
891
|
}
|
|
430
892
|
|
|
431
|
-
.
|
|
432
|
-
|
|
893
|
+
.error-message {
|
|
894
|
+
padding: 16px 20px;
|
|
895
|
+
background: #fff5f5;
|
|
896
|
+
border: 1px solid #fca5a5;
|
|
897
|
+
border-left: 4px solid #f56565;
|
|
898
|
+
border-radius: 8px;
|
|
899
|
+
margin-bottom: 20px;
|
|
900
|
+
display: flex;
|
|
901
|
+
justify-content: space-between;
|
|
902
|
+
align-items: center;
|
|
903
|
+
color: #c53030;
|
|
433
904
|
}
|
|
434
905
|
|
|
435
|
-
.
|
|
436
|
-
list-style: none;
|
|
437
|
-
padding: 0;
|
|
438
|
-
margin: 0 0 20px 0;
|
|
439
|
-
}
|
|
906
|
+
.error-message strong { font-weight: 600; }
|
|
440
907
|
|
|
441
|
-
.
|
|
442
|
-
|
|
443
|
-
|
|
908
|
+
.retry-btn {
|
|
909
|
+
background: #f56565;
|
|
910
|
+
padding: 8px 16px;
|
|
911
|
+
font-size: 12px;
|
|
912
|
+
margin-left: 15px;
|
|
444
913
|
border-radius: 4px;
|
|
445
|
-
margin-bottom: 10px;
|
|
446
914
|
}
|
|
447
915
|
|
|
448
|
-
.
|
|
449
|
-
|
|
450
|
-
|
|
916
|
+
.retry-btn:hover { background: #e53e3e; }
|
|
917
|
+
|
|
918
|
+
.docs-header {
|
|
919
|
+
display: flex;
|
|
920
|
+
justify-content: space-between;
|
|
921
|
+
align-items: center;
|
|
922
|
+
margin-bottom: 24px;
|
|
923
|
+
gap: 20px;
|
|
451
924
|
}
|
|
452
925
|
|
|
453
|
-
.
|
|
926
|
+
.docs-header h2 {
|
|
454
927
|
margin: 0;
|
|
928
|
+
color: #1a202c;
|
|
929
|
+
font-size: 26px;
|
|
930
|
+
flex: 1;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
.docs-controls {
|
|
934
|
+
display: flex;
|
|
935
|
+
gap: 12px;
|
|
936
|
+
flex: 1;
|
|
937
|
+
max-width: 600px;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
.search-input {
|
|
941
|
+
flex: 1;
|
|
942
|
+
padding: 10px 16px;
|
|
943
|
+
border: 1px solid #e2e8f0;
|
|
944
|
+
border-radius: 6px;
|
|
945
|
+
font-size: 14px;
|
|
946
|
+
transition: all 0.2s ease;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
.search-input:focus {
|
|
950
|
+
outline: none;
|
|
951
|
+
border-color: #667eea;
|
|
952
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.loading {
|
|
956
|
+
text-align: center;
|
|
957
|
+
padding: 60px 20px;
|
|
958
|
+
color: #999;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.spinner {
|
|
962
|
+
display: inline-block;
|
|
963
|
+
width: 48px;
|
|
964
|
+
height: 48px;
|
|
965
|
+
border: 4px solid #e2e8f0;
|
|
966
|
+
border-top-color: #667eea;
|
|
967
|
+
border-radius: 50%;
|
|
968
|
+
animation: spin 0.8s linear infinite;
|
|
969
|
+
margin-bottom: 20px;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
973
|
+
|
|
974
|
+
.empty-state {
|
|
975
|
+
text-align: center;
|
|
976
|
+
padding: 60px 20px;
|
|
977
|
+
color: #999;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.empty-state p { margin: 8px 0; font-size: 16px; }
|
|
981
|
+
.empty-state .hint { font-size: 14px; opacity: 0.8; }
|
|
982
|
+
|
|
983
|
+
.docs-grid {
|
|
984
|
+
display: grid;
|
|
985
|
+
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
|
986
|
+
gap: 20px;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.doc-card {
|
|
990
|
+
background: #f9f9f9;
|
|
991
|
+
border: 1px solid #e2e8f0;
|
|
992
|
+
border-radius: 8px;
|
|
993
|
+
padding: 20px;
|
|
994
|
+
transition: all 0.3s ease;
|
|
995
|
+
display: flex;
|
|
996
|
+
flex-direction: column;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
.doc-card:hover {
|
|
1000
|
+
border-color: #667eea;
|
|
1001
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
|
|
1002
|
+
transform: translateY(-2px);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
.doc-card h3 {
|
|
1006
|
+
margin: 0 0 12px 0;
|
|
1007
|
+
color: #1a202c;
|
|
1008
|
+
font-size: 18px;
|
|
1009
|
+
font-weight: 600;
|
|
1010
|
+
word-break: break-word;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
.doc-preview {
|
|
1014
|
+
flex: 1;
|
|
1015
|
+
margin: 0 0 12px 0;
|
|
455
1016
|
color: #666;
|
|
456
1017
|
font-size: 14px;
|
|
1018
|
+
line-height: 1.6;
|
|
1019
|
+
display: -webkit-box;
|
|
1020
|
+
-webkit-line-clamp: 3;
|
|
1021
|
+
-webkit-box-orient: vertical;
|
|
1022
|
+
overflow: hidden;
|
|
457
1023
|
}
|
|
458
1024
|
|
|
459
|
-
|
|
1025
|
+
.doc-meta {
|
|
1026
|
+
display: flex;
|
|
1027
|
+
justify-content: space-between;
|
|
1028
|
+
align-items: center;
|
|
1029
|
+
padding: 12px 0;
|
|
1030
|
+
border-top: 1px solid #e2e8f0;
|
|
1031
|
+
border-bottom: 1px solid #e2e8f0;
|
|
1032
|
+
margin-bottom: 12px;
|
|
1033
|
+
font-size: 12px;
|
|
1034
|
+
color: #999;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.doc-size {
|
|
1038
|
+
background: #f0f4ff;
|
|
1039
|
+
padding: 2px 8px;
|
|
1040
|
+
border-radius: 4px;
|
|
1041
|
+
color: #667eea;
|
|
1042
|
+
font-weight: 600;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.doc-actions {
|
|
1046
|
+
display: flex;
|
|
1047
|
+
gap: 8px;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.edit-btn, .delete-btn, .save-btn, .cancel-btn {
|
|
1051
|
+
flex: 1;
|
|
1052
|
+
padding: 8px 12px;
|
|
1053
|
+
font-size: 13px;
|
|
1054
|
+
border: 1px solid;
|
|
1055
|
+
border-radius: 6px;
|
|
1056
|
+
cursor: pointer;
|
|
1057
|
+
font-weight: 600;
|
|
1058
|
+
transition: all 0.2s ease;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.edit-btn {
|
|
1062
|
+
background: #eef2ff;
|
|
1063
|
+
border-color: #667eea;
|
|
1064
|
+
color: #667eea;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.edit-btn:hover { background: #e0e7ff; }
|
|
1068
|
+
|
|
1069
|
+
.delete-btn {
|
|
1070
|
+
background: #fee;
|
|
1071
|
+
border-color: #fca5a5;
|
|
1072
|
+
color: #c53030;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
.delete-btn:hover:not(:disabled) { background: #fdd; }
|
|
1076
|
+
.delete-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
1077
|
+
|
|
1078
|
+
.edit-mode {
|
|
1079
|
+
display: flex;
|
|
1080
|
+
flex-direction: column;
|
|
1081
|
+
gap: 12px;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.edit-title, .edit-content {
|
|
1085
|
+
padding: 10px 12px;
|
|
1086
|
+
border: 1px solid #ddd;
|
|
1087
|
+
border-radius: 6px;
|
|
1088
|
+
font-family: inherit;
|
|
1089
|
+
font-size: 14px;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
.edit-title {
|
|
1093
|
+
font-size: 16px;
|
|
1094
|
+
font-weight: 600;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.edit-title:focus, .edit-content:focus {
|
|
1098
|
+
outline: none;
|
|
1099
|
+
border-color: #667eea;
|
|
1100
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.edit-actions {
|
|
1104
|
+
display: flex;
|
|
1105
|
+
gap: 8px;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
.save-btn {
|
|
1109
|
+
background: #667eea;
|
|
1110
|
+
color: white;
|
|
1111
|
+
border: none;
|
|
1112
|
+
flex: 1;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
.save-btn:hover { background: #5568d3; }
|
|
1116
|
+
|
|
1117
|
+
.cancel-btn {
|
|
1118
|
+
background: #f0f0f0;
|
|
1119
|
+
color: #666;
|
|
1120
|
+
border: none;
|
|
1121
|
+
flex: 1;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.cancel-btn:hover { background: #e0e0e0; }
|
|
1125
|
+
|
|
1126
|
+
.primary-btn {
|
|
460
1127
|
background: #667eea;
|
|
461
1128
|
color: white;
|
|
462
1129
|
border: none;
|
|
463
1130
|
padding: 10px 20px;
|
|
464
|
-
|
|
465
|
-
|
|
1131
|
+
white-space: nowrap;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
.primary-btn:hover:not(:disabled) { background: #5568d3; }
|
|
1135
|
+
.primary-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
1136
|
+
|
|
1137
|
+
textarea, input {
|
|
1138
|
+
display: block;
|
|
1139
|
+
width: 100%;
|
|
1140
|
+
margin: 12px 0;
|
|
1141
|
+
padding: 12px;
|
|
1142
|
+
font-family: inherit;
|
|
466
1143
|
font-size: 14px;
|
|
467
|
-
|
|
1144
|
+
border: 1px solid #ddd;
|
|
1145
|
+
border-radius: 6px;
|
|
1146
|
+
resize: vertical;
|
|
468
1147
|
}
|
|
469
1148
|
|
|
470
|
-
|
|
471
|
-
|
|
1149
|
+
textarea:focus, input:focus {
|
|
1150
|
+
outline: none;
|
|
1151
|
+
border-color: #667eea;
|
|
1152
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
472
1153
|
}
|
|
473
1154
|
|
|
474
|
-
|
|
475
|
-
|
|
1155
|
+
.status-badge {
|
|
1156
|
+
display: inline-block;
|
|
1157
|
+
padding: 8px 12px;
|
|
1158
|
+
background: #f0f4ff;
|
|
1159
|
+
color: #667eea;
|
|
1160
|
+
border-radius: 6px;
|
|
1161
|
+
font-size: 12px;
|
|
1162
|
+
font-weight: 600;
|
|
1163
|
+
margin-bottom: 16px;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.researcher {
|
|
1167
|
+
border-top: 2px solid #e2e8f0;
|
|
1168
|
+
margin-top: 40px;
|
|
1169
|
+
padding-top: 30px;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.researcher h2 {
|
|
1173
|
+
margin-top: 0;
|
|
1174
|
+
color: #1a202c;
|
|
1175
|
+
font-size: 20px;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.research-btn {
|
|
1179
|
+
background: #667eea;
|
|
1180
|
+
color: white;
|
|
1181
|
+
border: none;
|
|
1182
|
+
padding: 12px 24px;
|
|
1183
|
+
margin: 16px 0;
|
|
1184
|
+
border-radius: 6px;
|
|
1185
|
+
cursor: pointer;
|
|
1186
|
+
font-weight: 600;
|
|
1187
|
+
transition: all 0.2s ease;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
.research-btn:hover:not(:disabled) { background: #5568d3; }
|
|
1191
|
+
.research-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
1192
|
+
|
|
1193
|
+
.research-result {
|
|
1194
|
+
background: #f5f8ff;
|
|
1195
|
+
border: 1px solid #d4e0ff;
|
|
1196
|
+
border-radius: 8px;
|
|
1197
|
+
padding: 16px;
|
|
1198
|
+
margin-top: 20px;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
.ai-modes {
|
|
1202
|
+
display: flex;
|
|
1203
|
+
gap: 16px;
|
|
1204
|
+
margin-bottom: 16px;
|
|
1205
|
+
flex-wrap: wrap;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.mode-group {
|
|
1209
|
+
display: flex;
|
|
1210
|
+
flex-direction: column;
|
|
1211
|
+
gap: 6px;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
.mode-group-label {
|
|
1215
|
+
font-size: 11px;
|
|
1216
|
+
font-weight: 700;
|
|
1217
|
+
text-transform: uppercase;
|
|
1218
|
+
color: #999;
|
|
1219
|
+
letter-spacing: 0.5px;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
.mode-group {
|
|
1223
|
+
display: flex;
|
|
1224
|
+
gap: 6px;
|
|
1225
|
+
flex-wrap: wrap;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
.mode-btn {
|
|
1229
|
+
background: #f0f4ff;
|
|
1230
|
+
color: #667eea;
|
|
1231
|
+
border: 1px solid #d4e0ff;
|
|
1232
|
+
padding: 8px 14px;
|
|
1233
|
+
border-radius: 6px;
|
|
1234
|
+
cursor: pointer;
|
|
1235
|
+
font-size: 12px;
|
|
1236
|
+
font-weight: 600;
|
|
1237
|
+
transition: all 0.2s ease;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
.mode-btn:hover:not(:disabled) {
|
|
1241
|
+
background: #e0e7ff;
|
|
1242
|
+
border-color: #667eea;
|
|
1243
|
+
transform: translateY(-1px);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
.mode-btn.active {
|
|
1247
|
+
background: #667eea;
|
|
1248
|
+
color: white;
|
|
1249
|
+
border-color: #667eea;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.mode-btn.code-mode {
|
|
1253
|
+
background: #fef5e7;
|
|
1254
|
+
color: #c87832;
|
|
1255
|
+
border-color: #f4d29d;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
.mode-btn.code-mode:hover:not(:disabled) {
|
|
1259
|
+
background: #fdebd0;
|
|
1260
|
+
border-color: #c87832;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
.mode-btn.code-mode.active {
|
|
1264
|
+
background: #c87832;
|
|
1265
|
+
color: white;
|
|
1266
|
+
border-color: #c87832;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
.mode-btn:disabled {
|
|
1270
|
+
opacity: 0.5;
|
|
1271
|
+
cursor: not-allowed;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
.doc-selector {
|
|
1275
|
+
margin-bottom: 16px;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
.doc-selector label {
|
|
476
1279
|
display: block;
|
|
477
|
-
|
|
478
|
-
|
|
1280
|
+
font-size: 13px;
|
|
1281
|
+
font-weight: 600;
|
|
1282
|
+
color: #666;
|
|
1283
|
+
margin-bottom: 8px;
|
|
1284
|
+
text-transform: uppercase;
|
|
1285
|
+
letter-spacing: 0.5px;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.doc-selector select {
|
|
479
1289
|
width: 100%;
|
|
1290
|
+
padding: 10px 12px;
|
|
1291
|
+
border: 1px solid #ddd;
|
|
1292
|
+
border-radius: 6px;
|
|
1293
|
+
font-family: inherit;
|
|
1294
|
+
font-size: 14px;
|
|
1295
|
+
background: white;
|
|
480
1296
|
}
|
|
481
1297
|
|
|
482
|
-
.
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
1298
|
+
.doc-selector select:focus {
|
|
1299
|
+
outline: none;
|
|
1300
|
+
border-color: #667eea;
|
|
1301
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
.research-result h3 {
|
|
1305
|
+
margin: 0 0 12px 0;
|
|
1306
|
+
color: #667eea;
|
|
1307
|
+
font-size: 16px;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
.result-content {
|
|
1311
|
+
color: #555;
|
|
1312
|
+
line-height: 1.6;
|
|
1313
|
+
font-size: 14px;
|
|
1314
|
+
margin-bottom: 16px;
|
|
1315
|
+
max-height: 400px;
|
|
1316
|
+
overflow-y: auto;
|
|
1317
|
+
padding: 12px;
|
|
1318
|
+
background: white;
|
|
1319
|
+
border-radius: 4px;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
.result-actions {
|
|
1323
|
+
display: flex;
|
|
1324
|
+
gap: 8px;
|
|
1325
|
+
flex-wrap: wrap;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
.save-result-btn, .copy-result-btn {
|
|
1329
|
+
flex: 1;
|
|
1330
|
+
min-width: 140px;
|
|
1331
|
+
background: #48bb78;
|
|
1332
|
+
color: white;
|
|
1333
|
+
border: none;
|
|
1334
|
+
padding: 10px 16px;
|
|
1335
|
+
border-radius: 6px;
|
|
1336
|
+
cursor: pointer;
|
|
1337
|
+
font-weight: 600;
|
|
1338
|
+
font-size: 13px;
|
|
1339
|
+
transition: all 0.2s ease;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
.save-result-btn:hover {
|
|
1343
|
+
background: #38a169;
|
|
1344
|
+
transform: translateY(-1px);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
.copy-result-btn {
|
|
1348
|
+
background: #4299e1;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
.copy-result-btn:hover {
|
|
1352
|
+
background: #3182ce;
|
|
1353
|
+
transform: translateY(-1px);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
.code-note {
|
|
1357
|
+
font-size: 12px;
|
|
1358
|
+
color: #999;
|
|
1359
|
+
margin-top: 12px;
|
|
1360
|
+
font-style: italic;
|
|
1361
|
+
border-top: 1px solid #e2e8f0;
|
|
1362
|
+
padding-top: 12px;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
@media (max-width: 768px) {
|
|
1366
|
+
.docs-header {
|
|
1367
|
+
flex-direction: column;
|
|
1368
|
+
align-items: stretch;
|
|
1369
|
+
}
|
|
1370
|
+
.docs-controls {
|
|
1371
|
+
flex-direction: column;
|
|
1372
|
+
max-width: 100%;
|
|
1373
|
+
}
|
|
1374
|
+
.docs-grid {
|
|
1375
|
+
grid-template-columns: 1fr;
|
|
1376
|
+
}
|
|
1377
|
+
header { padding: 24px 20px; }
|
|
1378
|
+
main { padding: 20px; }
|
|
486
1379
|
}
|
|
487
1380
|
\`}</style>
|
|
488
1381
|
</div>
|
|
@@ -534,13 +1427,263 @@ main().catch(console.error);
|
|
|
534
1427
|
fs.writeFileSync(path.join(projectDir, 'index.html'), indexHtml);
|
|
535
1428
|
// Create .env.example
|
|
536
1429
|
const envExample = `# FeltDB Configuration
|
|
1430
|
+
# Copy this file to .env.local and update the values
|
|
1431
|
+
|
|
1432
|
+
# API Key for authenticating with FeltDB servers
|
|
1433
|
+
# Leave empty for browser runtime, required for self-hosted
|
|
537
1434
|
VITE_FELTDB_API_KEY=
|
|
1435
|
+
|
|
1436
|
+
# FeltDB Server URL (for self-hosted runtime)
|
|
538
1437
|
VITE_FELTDB_URL=http://localhost:7700
|
|
539
|
-
|
|
1438
|
+
|
|
1439
|
+
# Override the default self-hosted container image
|
|
1440
|
+
# Example: ghcr.io/rkendel1/feltdb:latest
|
|
540
1441
|
FELTDB_IMAGE=
|
|
1442
|
+
|
|
1443
|
+
# Node environment
|
|
541
1444
|
NODE_ENV=development
|
|
542
1445
|
`;
|
|
543
1446
|
fs.writeFileSync(path.join(projectDir, '.env.example'), envExample);
|
|
1447
|
+
// Create RUNTIME_GUIDE.md
|
|
1448
|
+
const runtimeGuide = `# Runtime Configuration Guide
|
|
1449
|
+
|
|
1450
|
+
This guide explains the differences between FeltDB runtime options and how to choose the right one for your use case.
|
|
1451
|
+
|
|
1452
|
+
## Browser Runtime
|
|
1453
|
+
|
|
1454
|
+
\`\`\`json
|
|
1455
|
+
{
|
|
1456
|
+
"runtime": "browser",
|
|
1457
|
+
"storage": "opfs"
|
|
1458
|
+
}
|
|
1459
|
+
\`\`\`
|
|
1460
|
+
|
|
1461
|
+
### What it does
|
|
1462
|
+
- Runs FeltDB entirely in the browser using JavaScript
|
|
1463
|
+
- Stores data in the browser's Origin Private File System (OPFS)
|
|
1464
|
+
- No backend server required
|
|
1465
|
+
|
|
1466
|
+
### When to use
|
|
1467
|
+
- Building offline-first applications
|
|
1468
|
+
- Client-side only projects
|
|
1469
|
+
- Prototyping and development
|
|
1470
|
+
- Privacy-focused applications where data never leaves the user's device
|
|
1471
|
+
|
|
1472
|
+
### Limitations
|
|
1473
|
+
- Data is isolated per browser/device
|
|
1474
|
+
- Single-user only
|
|
1475
|
+
- Limited by browser disk space (usually 50GB+)
|
|
1476
|
+
- Cannot be accessed from other devices/browsers
|
|
1477
|
+
|
|
1478
|
+
### Setup
|
|
1479
|
+
No special setup required. Just run \`npm run dev\`.
|
|
1480
|
+
|
|
1481
|
+
## Node.js Runtime
|
|
1482
|
+
|
|
1483
|
+
\`\`\`json
|
|
1484
|
+
{
|
|
1485
|
+
"runtime": "node",
|
|
1486
|
+
"storage": "durable"
|
|
1487
|
+
}
|
|
1488
|
+
\`\`\`
|
|
1489
|
+
|
|
1490
|
+
### What it does
|
|
1491
|
+
- Runs FeltDB as a Node.js server
|
|
1492
|
+
- Uses file-based or database storage
|
|
1493
|
+
- Accessible via HTTP/API
|
|
1494
|
+
|
|
1495
|
+
### When to use
|
|
1496
|
+
- Building backend APIs
|
|
1497
|
+
- Server-side applications
|
|
1498
|
+
- REST API backends
|
|
1499
|
+
- Integration with other services
|
|
1500
|
+
|
|
1501
|
+
### Limitations
|
|
1502
|
+
- Requires Node.js environment
|
|
1503
|
+
- Single server by default (no automatic distribution)
|
|
1504
|
+
- Data persistence depends on storage backend
|
|
1505
|
+
|
|
1506
|
+
### Setup
|
|
1507
|
+
\`\`\`bash
|
|
1508
|
+
npm run dev
|
|
1509
|
+
# or
|
|
1510
|
+
npm run feltdb:server
|
|
1511
|
+
\`\`\`
|
|
1512
|
+
|
|
1513
|
+
## Self-Hosted Runtime
|
|
1514
|
+
|
|
1515
|
+
\`\`\`json
|
|
1516
|
+
{
|
|
1517
|
+
"runtime": "self-hosted",
|
|
1518
|
+
"storage": "durable"
|
|
1519
|
+
}
|
|
1520
|
+
\`\`\`
|
|
1521
|
+
|
|
1522
|
+
### What it does
|
|
1523
|
+
- Runs a dedicated FeltDB server in Docker
|
|
1524
|
+
- Provides distributed capabilities
|
|
1525
|
+
- Enables multi-user, multi-device access
|
|
1526
|
+
- Includes API management and authentication
|
|
1527
|
+
|
|
1528
|
+
### When to use
|
|
1529
|
+
- Production deployments
|
|
1530
|
+
- Multi-user applications
|
|
1531
|
+
- Synchronization across devices
|
|
1532
|
+
- Team collaboration features
|
|
1533
|
+
- Advanced distributed scenarios
|
|
1534
|
+
|
|
1535
|
+
### Requirements
|
|
1536
|
+
- Docker installed and running
|
|
1537
|
+
- Internet access (first run downloads image)
|
|
1538
|
+
|
|
1539
|
+
### Setup
|
|
1540
|
+
\`\`\`bash
|
|
1541
|
+
npm run dev
|
|
1542
|
+
# Docker will be started automatically
|
|
1543
|
+
\`\`\`
|
|
1544
|
+
|
|
1545
|
+
The self-hosted instance runs on \`http://localhost:7700\` by default.
|
|
1546
|
+
|
|
1547
|
+
## Vector Search Status
|
|
1548
|
+
|
|
1549
|
+
### Current Status
|
|
1550
|
+
Vector search support in FeltDB is available through integrations with vector databases:
|
|
1551
|
+
- Milvus
|
|
1552
|
+
- Pinecone
|
|
1553
|
+
- Weaviate
|
|
1554
|
+
- Local vector storage (development)
|
|
1555
|
+
|
|
1556
|
+
### Enabling Vector Search
|
|
1557
|
+
To enable vector search capabilities:
|
|
1558
|
+
|
|
1559
|
+
1. Update \`feltdb.config.json\`:
|
|
1560
|
+
\`\`\`json
|
|
1561
|
+
{
|
|
1562
|
+
"capabilities": {
|
|
1563
|
+
"search": true,
|
|
1564
|
+
"vector-search": true
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
\`\`\`
|
|
1568
|
+
|
|
1569
|
+
2. Configure your vector database in your environment variables or code
|
|
1570
|
+
|
|
1571
|
+
3. Implement vector embedding logic in your capabilities:
|
|
1572
|
+
\`\`\`typescript
|
|
1573
|
+
// feeldb/capabilities/index.ts
|
|
1574
|
+
export const capabilities = {
|
|
1575
|
+
'vector-search': {
|
|
1576
|
+
enabled: true,
|
|
1577
|
+
scope: ['documents:read', 'capabilities:execute'],
|
|
1578
|
+
},
|
|
1579
|
+
};
|
|
1580
|
+
\`\`\`
|
|
1581
|
+
|
|
1582
|
+
### Vector Storage Options
|
|
1583
|
+
|
|
1584
|
+
#### Local Development
|
|
1585
|
+
For development, FeltDB includes a local vector store:
|
|
1586
|
+
- In-memory embeddings
|
|
1587
|
+
- No external dependencies
|
|
1588
|
+
- Perfect for prototyping
|
|
1589
|
+
|
|
1590
|
+
#### Production
|
|
1591
|
+
For production deployments:
|
|
1592
|
+
|
|
1593
|
+
**Milvus** (Open source)
|
|
1594
|
+
- Self-hosted vector database
|
|
1595
|
+
- Scalable to millions of vectors
|
|
1596
|
+
- Full-text + vector search combined
|
|
1597
|
+
|
|
1598
|
+
**Pinecone** (Managed service)
|
|
1599
|
+
- Serverless vector database
|
|
1600
|
+
- Easy to set up
|
|
1601
|
+
- No infrastructure to manage
|
|
1602
|
+
|
|
1603
|
+
**Weaviate** (Open source + Cloud)
|
|
1604
|
+
- GraphQL interface
|
|
1605
|
+
- Hybrid search (text + vectors)
|
|
1606
|
+
- Multiple deployment options
|
|
1607
|
+
|
|
1608
|
+
## Migration Between Runtimes
|
|
1609
|
+
|
|
1610
|
+
You can migrate between runtimes by:
|
|
1611
|
+
|
|
1612
|
+
1. Export data from current runtime
|
|
1613
|
+
2. Update \`feltdb.config.json\` with new runtime
|
|
1614
|
+
3. Import data to new runtime
|
|
1615
|
+
|
|
1616
|
+
Example:
|
|
1617
|
+
\`\`\`bash
|
|
1618
|
+
# Export from browser
|
|
1619
|
+
npm run feltdb:export
|
|
1620
|
+
|
|
1621
|
+
# Update config
|
|
1622
|
+
# nano feltdb.config.json
|
|
1623
|
+
|
|
1624
|
+
# Start new runtime
|
|
1625
|
+
npm run dev
|
|
1626
|
+
|
|
1627
|
+
# Import data
|
|
1628
|
+
npm run feltdb:import
|
|
1629
|
+
\`\`\`
|
|
1630
|
+
|
|
1631
|
+
## Environment Variables by Runtime
|
|
1632
|
+
|
|
1633
|
+
### Browser
|
|
1634
|
+
- No special variables needed
|
|
1635
|
+
- Optional: \`VITE_DEBUG=1\` for debugging
|
|
1636
|
+
|
|
1637
|
+
### Node.js
|
|
1638
|
+
- \`DATABASE_URL\`: Connection string for persistent database
|
|
1639
|
+
- \`PORT\`: Server port (default: 3000)
|
|
1640
|
+
- \`NODE_ENV\`: development|production
|
|
1641
|
+
|
|
1642
|
+
### Self-Hosted
|
|
1643
|
+
- \`VITE_FELTDB_URL\`: Docker container URL (default: http://localhost:7700)
|
|
1644
|
+
- \`VITE_FELTDB_API_KEY\`: API token for authentication
|
|
1645
|
+
- \`FELTDB_IMAGE\`: Docker image to use (optional override)
|
|
1646
|
+
- \`DOCKER_NETWORK\`: Custom Docker network (optional)
|
|
1647
|
+
|
|
1648
|
+
## Troubleshooting
|
|
1649
|
+
|
|
1650
|
+
### "Runtime not configured" Error
|
|
1651
|
+
Update \`feltdb.config.json\` with a valid runtime selection.
|
|
1652
|
+
|
|
1653
|
+
### "Cannot connect to self-hosted server"
|
|
1654
|
+
Check that Docker is running:
|
|
1655
|
+
\`\`\`bash
|
|
1656
|
+
docker ps | grep feltdb
|
|
1657
|
+
\`\`\`
|
|
1658
|
+
|
|
1659
|
+
If not running, restart:
|
|
1660
|
+
\`\`\`bash
|
|
1661
|
+
npm run dev
|
|
1662
|
+
\`\`\`
|
|
1663
|
+
|
|
1664
|
+
### Browser Storage Full
|
|
1665
|
+
OPFS limit reached. Clear unused data or migrate to self-hosted.
|
|
1666
|
+
|
|
1667
|
+
### Vector Search Not Working
|
|
1668
|
+
Verify vector database connection and that embeddings are enabled in capabilities.
|
|
1669
|
+
|
|
1670
|
+
## Performance Considerations
|
|
1671
|
+
|
|
1672
|
+
| Metric | Browser | Node.js | Self-Hosted |
|
|
1673
|
+
|--------|---------|---------|-------------|
|
|
1674
|
+
| Latency | Immediate | Low | Low |
|
|
1675
|
+
| Throughput | ~1000 ops/sec | ~10K ops/sec | ~100K ops/sec |
|
|
1676
|
+
| Data Limit | 50GB+ | Disk dependent | Unlimited |
|
|
1677
|
+
| Users | 1 | Few | Many |
|
|
1678
|
+
| Cost | Free | Hosting cost | Hosting cost |
|
|
1679
|
+
|
|
1680
|
+
## Next Steps
|
|
1681
|
+
|
|
1682
|
+
- Read the [FeltDB Documentation](https://github.com/rkendel1/feltdb)
|
|
1683
|
+
- Check out example projects in the templates
|
|
1684
|
+
- Join the community Discord for support
|
|
1685
|
+
`;
|
|
1686
|
+
fs.writeFileSync(path.join(projectDir, 'RUNTIME_GUIDE.md'), runtimeGuide);
|
|
544
1687
|
// Create .gitignore
|
|
545
1688
|
const gitignore = `node_modules/
|
|
546
1689
|
dist/
|
|
@@ -558,6 +1701,11 @@ build/
|
|
|
558
1701
|
const readme = `# ${applicationName}
|
|
559
1702
|
|
|
560
1703
|
A FeltDB distributed application with agents and capabilities.
|
|
1704
|
+
- **Runtime:** ${runtime}
|
|
1705
|
+
- **Framework:** ${framework}
|
|
1706
|
+
- **Distributed:** ${distributed ? 'Yes' : 'No'}
|
|
1707
|
+
- **Agents:** ${hasAgents ? 'Yes' : 'No'}
|
|
1708
|
+
- **Capabilities:** ${capabilities}
|
|
561
1709
|
|
|
562
1710
|
## Quick Start
|
|
563
1711
|
|
|
@@ -566,38 +1714,74 @@ npm install
|
|
|
566
1714
|
npm run dev
|
|
567
1715
|
\`\`\`
|
|
568
1716
|
|
|
1717
|
+
The application will be available at http://localhost:5173.
|
|
1718
|
+
|
|
569
1719
|
## Project Structure
|
|
570
1720
|
|
|
571
1721
|
\`\`\`
|
|
572
1722
|
${applicationName}/
|
|
573
|
-
├── feltdb/
|
|
1723
|
+
├── feltdb/ # FeltDB configuration and logic
|
|
574
1724
|
│ ├── agents/ # Agent definitions
|
|
575
1725
|
│ ├── capabilities/ # Capability implementations
|
|
576
1726
|
│ ├── workflows/ # Workflow definitions
|
|
577
1727
|
│ └── schema/ # Data schemas
|
|
578
|
-
├── src/
|
|
1728
|
+
├── src/ # Application source code
|
|
579
1729
|
│ ├── App.${framework === 'react' ? 'tsx' : 'js'}
|
|
580
|
-
│ ├── feltdb.ts
|
|
1730
|
+
│ ├── feltdb.ts # FeltDB client initialization
|
|
581
1731
|
│ └── index.${framework === 'react' ? 'tsx' : 'js'}
|
|
582
|
-
├── public/
|
|
1732
|
+
├── public/ # Static assets
|
|
583
1733
|
├── index.html
|
|
584
1734
|
├── .feltdb/ # Local FeltDB configuration
|
|
585
|
-
├── feltdb.config.json
|
|
586
|
-
├── .env.example
|
|
1735
|
+
├── feltdb.config.json # FeltDB configuration
|
|
1736
|
+
├── .env.example # Environment variables template
|
|
587
1737
|
├── package.json
|
|
588
1738
|
├── tsconfig.json
|
|
589
1739
|
└── README.md
|
|
590
1740
|
\`\`\`
|
|
591
1741
|
|
|
1742
|
+
## Runtime Options
|
|
1743
|
+
|
|
1744
|
+
### Browser (\`browser\`)
|
|
1745
|
+
- Local-first, client-side only
|
|
1746
|
+
- Uses browser OPFS (Origin Private File System) for storage
|
|
1747
|
+
- No server required
|
|
1748
|
+
- Best for: Offline-first apps, privacy-focused applications
|
|
1749
|
+
- Limitations: Single device scope (data stays local)
|
|
1750
|
+
|
|
1751
|
+
### Node.js (\`node\`)
|
|
1752
|
+
- Server-side Node.js runtime
|
|
1753
|
+
- In-memory or file-based storage
|
|
1754
|
+
- Suitable for APIs and backend services
|
|
1755
|
+
- Best for: Server-side applications, REST APIs
|
|
1756
|
+
- Limitations: Memory-based by default
|
|
1757
|
+
|
|
1758
|
+
### Self-Hosted (\`self-hosted\`)
|
|
1759
|
+
- Dedicated FeltDB server instance
|
|
1760
|
+
- Durable storage with distributed capabilities
|
|
1761
|
+
- Requires Docker (image: ghcr.io/rkendel1/feltdb)
|
|
1762
|
+
- Best for: Production deployments, multi-user systems
|
|
1763
|
+
- Setup: \`npm run dev\` starts the Docker container automatically
|
|
1764
|
+
|
|
592
1765
|
## Configuration
|
|
593
1766
|
|
|
594
1767
|
Configuration is in \`feltdb.config.json\`:
|
|
595
|
-
- \`runtime\`: ${runtime}
|
|
1768
|
+
- \`runtime\`: ${runtime}
|
|
596
1769
|
- \`storage\`: ${runtime === 'browser' ? 'opfs' : 'durable'}
|
|
597
1770
|
- \`distributed\`: ${distributed}
|
|
598
1771
|
- \`agents.enabled\`: ${hasAgents}
|
|
599
1772
|
- \`capabilities\`: ${capabilities}
|
|
600
1773
|
|
|
1774
|
+
### Environment Variables
|
|
1775
|
+
|
|
1776
|
+
Create a \`.env.local\` file (copy from \`.env.example\`):
|
|
1777
|
+
|
|
1778
|
+
\`\`\`
|
|
1779
|
+
VITE_FELTDB_API_KEY=your_api_key_here
|
|
1780
|
+
VITE_FELTDB_URL=http://localhost:7700
|
|
1781
|
+
\`\`\`
|
|
1782
|
+
|
|
1783
|
+
These are used when connecting to a self-hosted FeltDB instance.
|
|
1784
|
+
|
|
601
1785
|
## Development
|
|
602
1786
|
|
|
603
1787
|
### Start Development Server
|
|
@@ -610,29 +1794,69 @@ npm run dev
|
|
|
610
1794
|
npm run build
|
|
611
1795
|
\`\`\`
|
|
612
1796
|
|
|
613
|
-
###
|
|
1797
|
+
### Validate FeltDB Configuration
|
|
614
1798
|
\`\`\`bash
|
|
615
|
-
|
|
1799
|
+
npm run feltdb:validate
|
|
616
1800
|
\`\`\`
|
|
617
1801
|
|
|
618
|
-
###
|
|
1802
|
+
### View FeltDB Status
|
|
619
1803
|
\`\`\`bash
|
|
620
|
-
|
|
1804
|
+
npm run feltdb:status
|
|
621
1805
|
\`\`\`
|
|
622
1806
|
|
|
1807
|
+
## Database Operations
|
|
1808
|
+
|
|
1809
|
+
### Collections
|
|
1810
|
+
|
|
1811
|
+
The application includes pre-configured collections:
|
|
1812
|
+
- \`documents\`: Stores research documents
|
|
1813
|
+
- \`reports\`: Stores generated reports
|
|
1814
|
+
|
|
1815
|
+
### Schema
|
|
1816
|
+
|
|
1817
|
+
Review \`feltdb.flow\` for the complete schema and workflow definitions.
|
|
1818
|
+
|
|
623
1819
|
## Agents
|
|
624
1820
|
|
|
625
|
-
${hasAgents ?
|
|
626
|
-
|
|
627
|
-
|
|
1821
|
+
${hasAgents ? `### Researcher Agent
|
|
1822
|
+
The \`researcher\` agent runs real private inference using \`@feltdb/webllm\`:
|
|
1823
|
+
- Runs entirely in the browser (no data sent to servers)
|
|
1824
|
+
- Model downloads on first use
|
|
1825
|
+
- Inference runs in a Web Worker
|
|
1826
|
+
- Generated reports are stored in FeltDB
|
|
1827
|
+
- Learn more: https://github.com/mlc-ai/web-llm` : 'No agents configured. Add agents by re-running create-feltdb.'}
|
|
1828
|
+
|
|
1829
|
+
## Vector Search
|
|
1830
|
+
|
|
1831
|
+
${capabilities.includes('vector') ? `Vector search is enabled. Configure your vector storage backend in \`feltdb.config.json\`.` : `Vector search is not enabled. To add it, update \`feltdb.config.json\` to include \`"vector-search": true\` in capabilities.`}
|
|
1832
|
+
|
|
1833
|
+
## Troubleshooting
|
|
1834
|
+
|
|
1835
|
+
### "Model not loaded" in Researcher
|
|
1836
|
+
If the WebLLM Researcher shows "Model not loaded":
|
|
1837
|
+
1. Check browser console for errors
|
|
1838
|
+
2. Ensure sufficient disk space (models are ~2-3GB)
|
|
1839
|
+
3. Try in a private/incognito window if localStorage is full
|
|
1840
|
+
4. Clear browser cache and try again
|
|
628
1841
|
|
|
629
|
-
|
|
1842
|
+
### Self-Hosted Connection Issues
|
|
1843
|
+
If using self-hosted mode and connection fails:
|
|
1844
|
+
1. Ensure Docker is installed and running
|
|
1845
|
+
2. Check FELTDB_URL and API key in .env.local
|
|
1846
|
+
3. Run \`npm run feltdb:status\` to check server health
|
|
1847
|
+
4. View logs: \`docker logs feltdb\`
|
|
630
1848
|
|
|
631
|
-
|
|
1849
|
+
### API Key Errors
|
|
1850
|
+
If API key management fails in Studio:
|
|
1851
|
+
1. Verify VITE_FELTDB_URL is set correctly
|
|
1852
|
+
2. Ensure token has proper scopes
|
|
1853
|
+
3. Check CORS settings on self-hosted server
|
|
632
1854
|
|
|
633
1855
|
## Learn More
|
|
634
1856
|
|
|
635
|
-
|
|
1857
|
+
- [FeltDB Documentation](https://github.com/rkendel1/feltdb)
|
|
1858
|
+
- [WebLLM Documentation](https://github.com/mlc-ai/web-llm)
|
|
1859
|
+
- [Distribution & Capabilities](https://github.com/rkendel1/feltdb/docs/capabilities.md)
|
|
636
1860
|
`;
|
|
637
1861
|
fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
|
|
638
1862
|
}
|