@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.
@@ -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: 'Analyze documents with private, on-device inference',
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: ['Summarize documents without sending prompts off-device'],
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 ??= new WebLLMProvider({
199
- onProgress: ({ progress, text }) =>
200
- onProgress?.(\`${'${Math.round(progress * 100)}'}% ${'${text}'}\`),
201
- });
202
- const content = await provider.generate([
203
- { role: 'system', content: 'You are a concise research assistant. Keep all reasoning local.' },
204
- { role: 'user', content: prompt },
205
- ]);
206
- await reports.insert({
207
- title: prompt.slice(0, 80) || 'Local research',
208
- content,
209
- createdAt: new Date(),
210
- });
211
- return content;
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('Summarize why local-first applications are useful.');
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 handleResearch = async () => {
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
- setAnswer(await runResearcher(prompt, setModelStatus));
258
- setModelStatus('Ready inference stayed in this browser');
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>Private WebLLM Researcher</h2>
271
- <p>{modelStatus}</p>
272
- <textarea value={prompt} onChange={event => setPrompt(event.target.value)} rows={4} />
273
- <button onClick={handleResearch} disabled={generating || !prompt.trim()}>
274
- {generating ? 'Running locally…' : 'Run researcher'}
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
- {answer && <article><h3>Result</h3><p>{answer}</p></article>}
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
- useEffect(() => {
289
- const loadDocs = async () => {
290
- try {
291
- const allDocs = await documents.find({});
292
- setDocs(allDocs);
293
- } catch (err) {
294
- console.error('Error loading documents:', err);
295
- } finally {
296
- setLoading(false);
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
- await documents.insert({
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
- // Reload documents
311
- const allDocs = await documents.find({});
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
- <h2>Documents</h2>
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
- <p>Loading...</p>
743
+ <div className="loading">
744
+ <div className="spinner"></div>
745
+ <p>Loading documents...</p>
746
+ </div>
346
747
  ) : docs.length === 0 ? (
347
- <p>No documents yet</p>
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
- <ul>
350
- {docs.map((doc: any) => (
351
- <li key={doc.id}>
352
- <h3>{doc.title}</h3>
353
- <p>{doc.content}</p>
354
- </li>
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
- </ul>
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: #f5f5f5;
823
+ background: #f5f7fa;
824
+ color: #333;
369
825
  }
370
826
 
371
827
  .app {
372
- max-width: 1200px;
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: 8px;
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: 28px;
844
+ font-size: 32px;
845
+ font-weight: 700;
388
846
  }
389
847
 
390
848
  header p {
391
849
  margin: 0;
392
- opacity: 0.9;
850
+ opacity: 0.95;
851
+ font-size: 16px;
393
852
  }
394
853
 
395
854
  main {
396
855
  background: white;
397
- padding: 20px;
398
- border-radius: 8px;
399
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
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(200px, 1fr));
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: 15px;
411
- background: #f9f9f9;
412
- border-radius: 4px;
413
- border-left: 3px solid #667eea;
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: #666;
419
- font-size: 12px;
878
+ color: #999;
879
+ font-size: 10px;
420
880
  text-transform: uppercase;
421
- margin-bottom: 5px;
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: 24px;
427
- font-weight: bold;
428
- color: #333;
888
+ font-size: 32px;
889
+ font-weight: 700;
890
+ color: #667eea;
429
891
  }
430
892
 
431
- .documents h2 {
432
- margin-top: 0;
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
- .documents ul {
436
- list-style: none;
437
- padding: 0;
438
- margin: 0 0 20px 0;
439
- }
906
+ .error-message strong { font-weight: 600; }
440
907
 
441
- .documents li {
442
- padding: 15px;
443
- background: #f9f9f9;
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
- .documents h3 {
449
- margin: 0 0 10px 0;
450
- color: #333;
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
- .documents p {
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
- button {
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
- border-radius: 4px;
465
- cursor: pointer;
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
- font-weight: 500;
1144
+ border: 1px solid #ddd;
1145
+ border-radius: 6px;
1146
+ resize: vertical;
468
1147
  }
469
1148
 
470
- button:hover {
471
- background: #5568d3;
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
- textarea {
475
- box-sizing: border-box;
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
- margin: 12px 0;
478
- padding: 10px;
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
- .researcher {
483
- border-top: 1px solid #eee;
484
- margin-top: 30px;
485
- padding-top: 20px;
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
- # Override the versioned self-hosted container when needed:
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} (browser|node|self-hosted)
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
- ### Connect to Remote Server
1797
+ ### Validate FeltDB Configuration
614
1798
  \`\`\`bash
615
- feltdb connect http://localhost:7700
1799
+ npm run feltdb:validate
616
1800
  \`\`\`
617
1801
 
618
- ### Create API Key
1802
+ ### View FeltDB Status
619
1803
  \`\`\`bash
620
- feltdb keys create --name development --scope '*'
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 ? `The \`researcher\` agent runs real private inference with
626
- \`@feltdb/webllm\`. The model downloads on first use, runs in a Web Worker, and
627
- caches its artifacts in the browser. Generated reports are stored in FeltDB.` : 'No agents configured'}
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
- ## Capabilities
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
- ${capabilities}
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
- Visit [FeltDB Documentation](https://github.com/rkendel1/feltdb) to learn more.
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
  }