@feltdb/core 0.4.5 → 0.4.7
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/README.md +9 -0
- package/bin/create-feltdb.js +3 -0
- package/bin/feltdb.js +3 -0
- package/dist/analytics-backend.js +1 -1
- package/dist/cli/api-client.js +236 -0
- package/dist/cli/cli.js +18 -0
- package/dist/cli/commands.js +795 -0
- package/dist/cli/config.js +66 -0
- package/dist/cli/index.js +399 -0
- package/dist/create/application-identity.js +132 -0
- package/dist/create/cli-scripts-generator.js +211 -0
- package/dist/create/cli.js +251 -0
- package/dist/create/create.js +1862 -0
- package/dist/create/docker-compose-generator.js +258 -0
- package/dist/create/index.js +4 -0
- package/dist/create/package-versions.js +4 -0
- package/dist/create/runtime-templates.js +272 -0
- package/dist/index-backend.js +1 -1
- package/dist/react/useFeltDB.d.ts +1 -1
- package/dist/react/useFeltDB.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.d.ts +461 -0
- package/dist/wasm/feltdb_wasm.js +1690 -0
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/wasm/feltdb_wasm_bg.wasm.d.ts +84 -0
- package/dist/wasm/package.json +16 -0
- package/package.json +11 -5
- package/dist/studio/KeyManagementPanel-BOvWTRyH.js +0 -246
- package/dist/studio-app/assets/feltdb_wasm_bg-bYYbZeRM.wasm +0 -0
|
@@ -0,0 +1,1862 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project creation logic
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { feltdbPackageRange } from './package-versions.js';
|
|
7
|
+
export async function createProject(options) {
|
|
8
|
+
const { projectName, templatesDir } = options;
|
|
9
|
+
const projectDir = path.resolve(process.cwd(), projectName);
|
|
10
|
+
const applicationName = path.basename(projectDir);
|
|
11
|
+
// Create project directory
|
|
12
|
+
if (!fs.existsSync(projectDir)) {
|
|
13
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
// Create feltdb directory structure
|
|
16
|
+
const feltdbDir = path.join(projectDir, 'feltdb');
|
|
17
|
+
const srcDir = path.join(projectDir, 'src');
|
|
18
|
+
const publicDir = path.join(projectDir, 'public');
|
|
19
|
+
const feltdbConfigDir = path.join(projectDir, '.feltdb');
|
|
20
|
+
fs.mkdirSync(path.join(feltdbDir, 'capabilities'), { recursive: true });
|
|
21
|
+
fs.mkdirSync(path.join(feltdbDir, 'workflows'), { recursive: true });
|
|
22
|
+
fs.mkdirSync(path.join(feltdbDir, 'agents'), { recursive: true });
|
|
23
|
+
fs.mkdirSync(path.join(feltdbDir, 'schema'), { recursive: true });
|
|
24
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
25
|
+
fs.mkdirSync(publicDir, { recursive: true });
|
|
26
|
+
fs.mkdirSync(feltdbConfigDir, { recursive: true });
|
|
27
|
+
const runtime = options.runtime || 'browser';
|
|
28
|
+
const framework = options.framework || 'react';
|
|
29
|
+
const distributed = options.distributed !== false;
|
|
30
|
+
const hasAgents = options.agents !== false;
|
|
31
|
+
const capabilities = options.capabilities || 'search';
|
|
32
|
+
// Create package.json
|
|
33
|
+
const packageJson = {
|
|
34
|
+
name: applicationName,
|
|
35
|
+
version: '0.1.0',
|
|
36
|
+
description: `A FeltDB application`,
|
|
37
|
+
main: 'src/index.ts',
|
|
38
|
+
type: 'module',
|
|
39
|
+
scripts: {
|
|
40
|
+
dev: 'feltdb dev',
|
|
41
|
+
build: 'feltdb build',
|
|
42
|
+
test: 'node --test',
|
|
43
|
+
feltdb: 'feltdb',
|
|
44
|
+
'feltdb:server': 'feltdb server',
|
|
45
|
+
'feltdb:connect': 'feltdb connect http://localhost:7700',
|
|
46
|
+
'feltdb:status': 'feltdb status',
|
|
47
|
+
'feltdb:studio': 'feltdb studio',
|
|
48
|
+
'feltdb:validate': 'feltdb validate',
|
|
49
|
+
'feltdb:diff': 'feltdb diff',
|
|
50
|
+
'feltdb:deploy': 'feltdb deploy',
|
|
51
|
+
},
|
|
52
|
+
dependencies: {
|
|
53
|
+
'@feltdb/core': feltdbPackageRange,
|
|
54
|
+
},
|
|
55
|
+
devDependencies: {
|
|
56
|
+
typescript: '^5.0.0',
|
|
57
|
+
'@types/node': '^20.0.0',
|
|
58
|
+
vite: '^8.2.1',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
if (framework === 'react') {
|
|
62
|
+
packageJson.dependencies['react'] = '^18.0.0';
|
|
63
|
+
packageJson.dependencies['react-dom'] = '^18.0.0';
|
|
64
|
+
packageJson.devDependencies['@types/react'] = '^18.0.0';
|
|
65
|
+
packageJson.devDependencies['@types/react-dom'] = '^18.0.0';
|
|
66
|
+
}
|
|
67
|
+
if (hasAgents) {
|
|
68
|
+
packageJson.dependencies['@feltdb/webllm'] = feltdbPackageRange;
|
|
69
|
+
}
|
|
70
|
+
fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
71
|
+
// Create feltdb.config.json
|
|
72
|
+
const feltdbConfig = {
|
|
73
|
+
namespace: applicationName,
|
|
74
|
+
runtime,
|
|
75
|
+
storage: runtime === 'browser' ? 'opfs' : 'durable',
|
|
76
|
+
distributed,
|
|
77
|
+
agents: {
|
|
78
|
+
enabled: hasAgents,
|
|
79
|
+
},
|
|
80
|
+
capabilities: {},
|
|
81
|
+
};
|
|
82
|
+
if (capabilities.includes('search')) {
|
|
83
|
+
feltdbConfig.capabilities.search = true;
|
|
84
|
+
}
|
|
85
|
+
if (capabilities.includes('vector')) {
|
|
86
|
+
feltdbConfig.capabilities['vector-search'] = true;
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(path.join(projectDir, 'feltdb.config.json'), JSON.stringify(feltdbConfig, null, 2));
|
|
89
|
+
const appName = applicationName.replace(/[^A-Za-z0-9_]/g, '_').replace(/^[^A-Za-z_]/, 'App_');
|
|
90
|
+
const flowSpec = `app ${appName} {
|
|
91
|
+
collection Document {
|
|
92
|
+
title: text
|
|
93
|
+
content: text
|
|
94
|
+
createdAt: datetime
|
|
95
|
+
index search using fulltext(content)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
collection Report {
|
|
99
|
+
title: text
|
|
100
|
+
content: text
|
|
101
|
+
document: ref Document
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
capability Research {
|
|
105
|
+
read Document
|
|
106
|
+
write Report
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
agent Researcher {
|
|
110
|
+
capability Research
|
|
111
|
+
workflow ResearchDocument
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
workflow ResearchDocument(document: Document) {
|
|
115
|
+
step search {
|
|
116
|
+
input document.content
|
|
117
|
+
}
|
|
118
|
+
step identity {
|
|
119
|
+
input search.output
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
trigger on Document.created {
|
|
124
|
+
workflow ResearchDocument(document)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
policy Document {
|
|
128
|
+
read: authenticated
|
|
129
|
+
write: authenticated
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
fs.writeFileSync(path.join(projectDir, 'feltdb.flow'), flowSpec);
|
|
134
|
+
// Create tsconfig.json
|
|
135
|
+
const tsconfig = {
|
|
136
|
+
compilerOptions: {
|
|
137
|
+
target: 'ES2020',
|
|
138
|
+
module: 'ESNext',
|
|
139
|
+
lib: ['ES2020', 'DOM'],
|
|
140
|
+
declaration: true,
|
|
141
|
+
outDir: './dist',
|
|
142
|
+
rootDir: './src',
|
|
143
|
+
strict: true,
|
|
144
|
+
esModuleInterop: true,
|
|
145
|
+
skipLibCheck: true,
|
|
146
|
+
forceConsistentCasingInFileNames: true,
|
|
147
|
+
moduleResolution: 'node',
|
|
148
|
+
},
|
|
149
|
+
include: ['src/**/*'],
|
|
150
|
+
exclude: ['node_modules'],
|
|
151
|
+
};
|
|
152
|
+
if (framework === 'react') {
|
|
153
|
+
tsconfig.compilerOptions.jsx = 'react-jsx';
|
|
154
|
+
}
|
|
155
|
+
fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
|
|
156
|
+
// Create main application files
|
|
157
|
+
const runtimeOptions = runtime === 'browser'
|
|
158
|
+
? "{ namespace: '" + applicationName + "', browser: true }"
|
|
159
|
+
: runtime === 'self-hosted'
|
|
160
|
+
? "{ namespace: '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_URL || 'http://localhost:7700', token: import.meta.env.VITE_FELTDB_API_KEY || '' } }"
|
|
161
|
+
: "{ namespace: '" + applicationName + "', memory: true }";
|
|
162
|
+
const feltdbTs = `import { createFeltDB } from '@feltdb/core';
|
|
163
|
+
|
|
164
|
+
export const db = createFeltDB(${runtimeOptions});
|
|
165
|
+
|
|
166
|
+
// Collections
|
|
167
|
+
export const documents = db.collection('documents');
|
|
168
|
+
export const reports = db.collection('reports');
|
|
169
|
+
`;
|
|
170
|
+
fs.writeFileSync(path.join(srcDir, 'feltdb.ts'), feltdbTs);
|
|
171
|
+
// Create a real local-inference agent for browser projects.
|
|
172
|
+
if (hasAgents) {
|
|
173
|
+
const agentTs = `import { WebLLMProvider } from '@feltdb/webllm';
|
|
174
|
+
import { db, reports } from '../../src/feltdb';
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Researcher metadata is registered in FeltDB; inference runs privately in
|
|
178
|
+
* the browser through WebLLM and its worker.
|
|
179
|
+
*/
|
|
180
|
+
export const researcher = db.defineAgent({
|
|
181
|
+
name: 'researcher',
|
|
182
|
+
version: 1,
|
|
183
|
+
description: 'Private, on-device AI assistant for document enhancement and analysis',
|
|
184
|
+
capabilities: [
|
|
185
|
+
'document-read',
|
|
186
|
+
'report-write'
|
|
187
|
+
],
|
|
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
|
+
],
|
|
193
|
+
constraints: { maxIterations: 1 },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
let provider: WebLLMProvider | undefined;
|
|
197
|
+
|
|
198
|
+
export async function runResearcher(
|
|
199
|
+
prompt: string,
|
|
200
|
+
onProgress?: (message: string) => void,
|
|
201
|
+
) {
|
|
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);
|
|
334
|
+
}
|
|
335
|
+
`;
|
|
336
|
+
fs.writeFileSync(path.join(feltdbDir, 'agents', 'researcher.ts'), agentTs);
|
|
337
|
+
}
|
|
338
|
+
// Create capabilities index
|
|
339
|
+
const capabilitiesTs = `/**
|
|
340
|
+
* Capabilities
|
|
341
|
+
*
|
|
342
|
+
* Define capabilities that can be executed by agents
|
|
343
|
+
* across distributed peers
|
|
344
|
+
*/
|
|
345
|
+
|
|
346
|
+
export const capabilities = {
|
|
347
|
+
'document-read': {
|
|
348
|
+
enabled: true,
|
|
349
|
+
scope: ['documents:read'],
|
|
350
|
+
},
|
|
351
|
+
'vector-search': {
|
|
352
|
+
enabled: ${capabilities.includes('vector')},
|
|
353
|
+
scope: ['documents:read', 'capabilities:execute'],
|
|
354
|
+
},
|
|
355
|
+
'report-write': {
|
|
356
|
+
enabled: true,
|
|
357
|
+
scope: ['reports:write'],
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
`;
|
|
361
|
+
fs.writeFileSync(path.join(feltdbDir, 'capabilities', 'index.ts'), capabilitiesTs);
|
|
362
|
+
// Create main application file based on framework
|
|
363
|
+
if (framework === 'react') {
|
|
364
|
+
const agentImport = hasAgents
|
|
365
|
+
? "import { runResearcher, summarizeDocument, enhanceDocument, generateIdeas, generateFeltDBFeature, generateReactComponent, generateCapability } from '../feltdb/agents/researcher';"
|
|
366
|
+
: '';
|
|
367
|
+
const agentState = hasAgents
|
|
368
|
+
? ` const [prompt, setPrompt] = useState('');
|
|
369
|
+
const [answer, setAnswer] = useState('');
|
|
370
|
+
const [modelStatus, setModelStatus] = useState('Model not loaded');
|
|
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');`
|
|
374
|
+
: '';
|
|
375
|
+
const agentHandler = hasAgents
|
|
376
|
+
? `
|
|
377
|
+
const handleRunAI = async (mode: 'custom' | 'summarize' | 'enhance' | 'ideas' | 'feltdb' | 'react' | 'capability') => {
|
|
378
|
+
setGenerating(true);
|
|
379
|
+
setAnswer('');
|
|
380
|
+
setModelStatus('Running inference...');
|
|
381
|
+
|
|
382
|
+
try {
|
|
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');
|
|
401
|
+
} catch (error) {
|
|
402
|
+
setModelStatus('✗ Error: ' + (error instanceof Error ? error.message : String(error)));
|
|
403
|
+
} finally {
|
|
404
|
+
setGenerating(false);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
`
|
|
408
|
+
: '';
|
|
409
|
+
const agentMarkup = hasAgents
|
|
410
|
+
? `
|
|
411
|
+
<section className="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'}
|
|
513
|
+
</button>
|
|
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
|
+
)}
|
|
573
|
+
</section>`
|
|
574
|
+
: '';
|
|
575
|
+
const appTsx = `import React, { useState, useEffect } from 'react';
|
|
576
|
+
import { db, documents } from './feltdb';
|
|
577
|
+
${agentImport}
|
|
578
|
+
|
|
579
|
+
export function App() {
|
|
580
|
+
const [docs, setDocs] = useState<any[]>([]);
|
|
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);
|
|
589
|
+
${agentState}
|
|
590
|
+
|
|
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
|
+
};
|
|
606
|
+
|
|
607
|
+
useEffect(() => {
|
|
608
|
+
loadDocs();
|
|
609
|
+
}, []);
|
|
610
|
+
|
|
611
|
+
const filteredDocs = docs.filter((doc: any) =>
|
|
612
|
+
doc.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
613
|
+
doc.content.toLowerCase().includes(searchTerm.toLowerCase())
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
const handleAddDocument = async () => {
|
|
617
|
+
if (addingDoc) return;
|
|
618
|
+
setAddingDoc(true);
|
|
619
|
+
setError(null);
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
const newDoc = {
|
|
623
|
+
id: 'doc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
|
|
624
|
+
title: 'New Document',
|
|
625
|
+
content: 'Enter your content here...',
|
|
626
|
+
createdAt: new Date().toISOString(),
|
|
627
|
+
};
|
|
628
|
+
await documents.insert(newDoc);
|
|
629
|
+
await loadDocs();
|
|
630
|
+
} catch (err) {
|
|
631
|
+
const msg = err instanceof Error ? err.message : 'Failed to add document';
|
|
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);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
${agentHandler}
|
|
688
|
+
|
|
689
|
+
return (
|
|
690
|
+
<div className="app">
|
|
691
|
+
<header>
|
|
692
|
+
<h1>🌊 FeltDB Research App</h1>
|
|
693
|
+
<p>Distributed document management with agents and capabilities</p>
|
|
694
|
+
</header>
|
|
695
|
+
|
|
696
|
+
<main>
|
|
697
|
+
<section className="stats">
|
|
698
|
+
<div className="stat">
|
|
699
|
+
<span className="label">Documents:</span>
|
|
700
|
+
<span className="value">{docs.length}</span>
|
|
701
|
+
</div>
|
|
702
|
+
<div className="stat">
|
|
703
|
+
<span className="label">Runtime:</span>
|
|
704
|
+
<span className="value">${runtime}</span>
|
|
705
|
+
</div>
|
|
706
|
+
<div className="stat">
|
|
707
|
+
<span className="label">Distributed:</span>
|
|
708
|
+
<span className="value">${distributed ? '✓' : '✗'}</span>
|
|
709
|
+
</div>
|
|
710
|
+
</section>
|
|
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
|
+
|
|
721
|
+
<section className="documents">
|
|
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
|
+
|
|
742
|
+
{loading ? (
|
|
743
|
+
<div className="loading">
|
|
744
|
+
<div className="spinner"></div>
|
|
745
|
+
<p>Loading documents...</p>
|
|
746
|
+
</div>
|
|
747
|
+
) : docs.length === 0 ? (
|
|
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>
|
|
756
|
+
) : (
|
|
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>
|
|
809
|
+
))}
|
|
810
|
+
</div>
|
|
811
|
+
)}
|
|
812
|
+
</section>
|
|
813
|
+
${agentMarkup}
|
|
814
|
+
</main>
|
|
815
|
+
|
|
816
|
+
<style>{\`
|
|
817
|
+
* { box-sizing: border-box; }
|
|
818
|
+
|
|
819
|
+
body {
|
|
820
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
|
|
821
|
+
margin: 0;
|
|
822
|
+
padding: 0;
|
|
823
|
+
background: #f5f7fa;
|
|
824
|
+
color: #333;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.app {
|
|
828
|
+
max-width: 1400px;
|
|
829
|
+
margin: 0 auto;
|
|
830
|
+
padding: 20px;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
header {
|
|
834
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
835
|
+
color: white;
|
|
836
|
+
padding: 40px 30px;
|
|
837
|
+
border-radius: 12px;
|
|
838
|
+
margin-bottom: 30px;
|
|
839
|
+
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
header h1 {
|
|
843
|
+
margin: 0 0 10px 0;
|
|
844
|
+
font-size: 32px;
|
|
845
|
+
font-weight: 700;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
header p {
|
|
849
|
+
margin: 0;
|
|
850
|
+
opacity: 0.95;
|
|
851
|
+
font-size: 16px;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
main {
|
|
855
|
+
background: white;
|
|
856
|
+
padding: 30px;
|
|
857
|
+
border-radius: 12px;
|
|
858
|
+
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.stats {
|
|
862
|
+
display: grid;
|
|
863
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
864
|
+
gap: 20px;
|
|
865
|
+
margin-bottom: 30px;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
.stat {
|
|
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);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.stat .label {
|
|
877
|
+
display: block;
|
|
878
|
+
color: #999;
|
|
879
|
+
font-size: 10px;
|
|
880
|
+
text-transform: uppercase;
|
|
881
|
+
letter-spacing: 1px;
|
|
882
|
+
margin-bottom: 8px;
|
|
883
|
+
font-weight: 700;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.stat .value {
|
|
887
|
+
display: block;
|
|
888
|
+
font-size: 32px;
|
|
889
|
+
font-weight: 700;
|
|
890
|
+
color: #667eea;
|
|
891
|
+
}
|
|
892
|
+
|
|
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;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.error-message strong { font-weight: 600; }
|
|
907
|
+
|
|
908
|
+
.retry-btn {
|
|
909
|
+
background: #f56565;
|
|
910
|
+
padding: 8px 16px;
|
|
911
|
+
font-size: 12px;
|
|
912
|
+
margin-left: 15px;
|
|
913
|
+
border-radius: 4px;
|
|
914
|
+
}
|
|
915
|
+
|
|
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;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.docs-header h2 {
|
|
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;
|
|
1016
|
+
color: #666;
|
|
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;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
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 {
|
|
1127
|
+
background: #667eea;
|
|
1128
|
+
color: white;
|
|
1129
|
+
border: none;
|
|
1130
|
+
padding: 10px 20px;
|
|
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;
|
|
1143
|
+
font-size: 14px;
|
|
1144
|
+
border: 1px solid #ddd;
|
|
1145
|
+
border-radius: 6px;
|
|
1146
|
+
resize: vertical;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
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);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
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 {
|
|
1279
|
+
display: block;
|
|
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 {
|
|
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;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
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; }
|
|
1379
|
+
}
|
|
1380
|
+
\`}</style>
|
|
1381
|
+
</div>
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
`;
|
|
1385
|
+
fs.writeFileSync(path.join(srcDir, 'App.tsx'), appTsx);
|
|
1386
|
+
const indexTsx = `import React from 'react';
|
|
1387
|
+
import ReactDOM from 'react-dom/client';
|
|
1388
|
+
import { App } from './App';
|
|
1389
|
+
|
|
1390
|
+
const root = ReactDOM.createRoot(document.getElementById('root')!);
|
|
1391
|
+
root.render(
|
|
1392
|
+
<React.StrictMode>
|
|
1393
|
+
<App />
|
|
1394
|
+
</React.StrictMode>
|
|
1395
|
+
);
|
|
1396
|
+
`;
|
|
1397
|
+
fs.writeFileSync(path.join(srcDir, 'index.tsx'), indexTsx);
|
|
1398
|
+
}
|
|
1399
|
+
else {
|
|
1400
|
+
// Create vanilla JS app
|
|
1401
|
+
const indexJs = `import { db, documents } from './feltdb';
|
|
1402
|
+
|
|
1403
|
+
async function main() {
|
|
1404
|
+
console.log('🌊 FeltDB Research App');
|
|
1405
|
+
console.log('Runtime: ${runtime}');
|
|
1406
|
+
console.log('Distributed: ${distributed}');
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
main().catch(console.error);
|
|
1410
|
+
`;
|
|
1411
|
+
fs.writeFileSync(path.join(srcDir, 'index.js'), indexJs);
|
|
1412
|
+
}
|
|
1413
|
+
// Create index.html
|
|
1414
|
+
const indexHtml = `<!DOCTYPE html>
|
|
1415
|
+
<html lang="en">
|
|
1416
|
+
<head>
|
|
1417
|
+
<meta charset="UTF-8">
|
|
1418
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1419
|
+
<title>${applicationName}</title>
|
|
1420
|
+
</head>
|
|
1421
|
+
<body>
|
|
1422
|
+
<div id="root"></div>
|
|
1423
|
+
<script type="module" src="/src/index${framework === 'react' ? '.tsx' : '.js'}"></script>
|
|
1424
|
+
</body>
|
|
1425
|
+
</html>
|
|
1426
|
+
`;
|
|
1427
|
+
fs.writeFileSync(path.join(projectDir, 'index.html'), indexHtml);
|
|
1428
|
+
// Create .env.example
|
|
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
|
|
1434
|
+
VITE_FELTDB_API_KEY=
|
|
1435
|
+
|
|
1436
|
+
# FeltDB Server URL (for self-hosted runtime)
|
|
1437
|
+
VITE_FELTDB_URL=http://localhost:7700
|
|
1438
|
+
|
|
1439
|
+
# Override the default self-hosted container image
|
|
1440
|
+
# Example: ghcr.io/rkendel1/feltdb:latest
|
|
1441
|
+
FELTDB_IMAGE=
|
|
1442
|
+
|
|
1443
|
+
# Node environment
|
|
1444
|
+
NODE_ENV=development
|
|
1445
|
+
`;
|
|
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);
|
|
1687
|
+
// Create .gitignore
|
|
1688
|
+
const gitignore = `node_modules/
|
|
1689
|
+
dist/
|
|
1690
|
+
build/
|
|
1691
|
+
.env
|
|
1692
|
+
.env.local
|
|
1693
|
+
.env.*.local
|
|
1694
|
+
*.log
|
|
1695
|
+
.DS_Store
|
|
1696
|
+
.feltdb/keys.json
|
|
1697
|
+
.feltdb/connection.json
|
|
1698
|
+
`;
|
|
1699
|
+
fs.writeFileSync(path.join(projectDir, '.gitignore'), gitignore);
|
|
1700
|
+
// Create README
|
|
1701
|
+
const readme = `# ${applicationName}
|
|
1702
|
+
|
|
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}
|
|
1709
|
+
|
|
1710
|
+
## Quick Start
|
|
1711
|
+
|
|
1712
|
+
\`\`\`bash
|
|
1713
|
+
npm install
|
|
1714
|
+
npm run dev
|
|
1715
|
+
\`\`\`
|
|
1716
|
+
|
|
1717
|
+
The application will be available at http://localhost:5173.
|
|
1718
|
+
|
|
1719
|
+
## Project Structure
|
|
1720
|
+
|
|
1721
|
+
\`\`\`
|
|
1722
|
+
${applicationName}/
|
|
1723
|
+
├── feltdb/ # FeltDB configuration and logic
|
|
1724
|
+
│ ├── agents/ # Agent definitions
|
|
1725
|
+
│ ├── capabilities/ # Capability implementations
|
|
1726
|
+
│ ├── workflows/ # Workflow definitions
|
|
1727
|
+
│ └── schema/ # Data schemas
|
|
1728
|
+
├── src/ # Application source code
|
|
1729
|
+
│ ├── App.${framework === 'react' ? 'tsx' : 'js'}
|
|
1730
|
+
│ ├── feltdb.ts # FeltDB client initialization
|
|
1731
|
+
│ └── index.${framework === 'react' ? 'tsx' : 'js'}
|
|
1732
|
+
├── public/ # Static assets
|
|
1733
|
+
├── index.html
|
|
1734
|
+
├── .feltdb/ # Local FeltDB configuration
|
|
1735
|
+
├── feltdb.config.json # FeltDB configuration
|
|
1736
|
+
├── .env.example # Environment variables template
|
|
1737
|
+
├── package.json
|
|
1738
|
+
├── tsconfig.json
|
|
1739
|
+
└── README.md
|
|
1740
|
+
\`\`\`
|
|
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
|
+
|
|
1765
|
+
## Configuration
|
|
1766
|
+
|
|
1767
|
+
Configuration is in \`feltdb.config.json\`:
|
|
1768
|
+
- \`runtime\`: ${runtime}
|
|
1769
|
+
- \`storage\`: ${runtime === 'browser' ? 'opfs' : 'durable'}
|
|
1770
|
+
- \`distributed\`: ${distributed}
|
|
1771
|
+
- \`agents.enabled\`: ${hasAgents}
|
|
1772
|
+
- \`capabilities\`: ${capabilities}
|
|
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
|
+
|
|
1785
|
+
## Development
|
|
1786
|
+
|
|
1787
|
+
### Start Development Server
|
|
1788
|
+
\`\`\`bash
|
|
1789
|
+
npm run dev
|
|
1790
|
+
\`\`\`
|
|
1791
|
+
|
|
1792
|
+
### Build for Production
|
|
1793
|
+
\`\`\`bash
|
|
1794
|
+
npm run build
|
|
1795
|
+
\`\`\`
|
|
1796
|
+
|
|
1797
|
+
### Validate FeltDB Configuration
|
|
1798
|
+
\`\`\`bash
|
|
1799
|
+
npm run feltdb:validate
|
|
1800
|
+
\`\`\`
|
|
1801
|
+
|
|
1802
|
+
### View FeltDB Status
|
|
1803
|
+
\`\`\`bash
|
|
1804
|
+
npm run feltdb:status
|
|
1805
|
+
\`\`\`
|
|
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
|
+
|
|
1819
|
+
## Agents
|
|
1820
|
+
|
|
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
|
|
1841
|
+
|
|
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\`
|
|
1848
|
+
|
|
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
|
|
1854
|
+
|
|
1855
|
+
## Learn More
|
|
1856
|
+
|
|
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)
|
|
1860
|
+
`;
|
|
1861
|
+
fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
|
|
1862
|
+
}
|