@chimerai/cli 0.2.73
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/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +317 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +2126 -0
- package/dist/commands/create.d.ts +12 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +1703 -0
- package/dist/commands/deploy.d.ts +11 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +219 -0
- package/dist/commands/dev.d.ts +17 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +206 -0
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +728 -0
- package/dist/commands/generate.d.ts +19 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +429 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +269 -0
- package/dist/commands/list.d.ts +12 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +328 -0
- package/dist/commands/migrate.d.ts +14 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +197 -0
- package/dist/commands/plugin.d.ts +10 -0
- package/dist/commands/plugin.d.ts.map +1 -0
- package/dist/commands/plugin.js +239 -0
- package/dist/commands/remove.d.ts +11 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +472 -0
- package/dist/commands/secret.d.ts +12 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/secret.js +102 -0
- package/dist/commands/setup.d.ts +9 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +788 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +211 -0
- package/dist/commands/use.d.ts +9 -0
- package/dist/commands/use.d.ts.map +1 -0
- package/dist/commands/use.js +51 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/license.d.ts +55 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +258 -0
- package/dist/scanner.d.ts +31 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +113 -0
- package/dist/schema-manager.d.ts +26 -0
- package/dist/schema-manager.d.ts.map +1 -0
- package/dist/schema-manager.js +132 -0
- package/dist/templates/admin.d.ts +49 -0
- package/dist/templates/admin.d.ts.map +1 -0
- package/dist/templates/admin.js +1358 -0
- package/dist/templates/ai-routes.d.ts +17 -0
- package/dist/templates/ai-routes.d.ts.map +1 -0
- package/dist/templates/ai-routes.js +1130 -0
- package/dist/templates/ai-service-tools.d.ts +22 -0
- package/dist/templates/ai-service-tools.d.ts.map +1 -0
- package/dist/templates/ai-service-tools.js +1424 -0
- package/dist/templates/ai-service.d.ts +66 -0
- package/dist/templates/ai-service.d.ts.map +1 -0
- package/dist/templates/ai-service.js +2202 -0
- package/dist/templates/api-routes.d.ts +108 -0
- package/dist/templates/api-routes.d.ts.map +1 -0
- package/dist/templates/api-routes.js +1219 -0
- package/dist/templates/auth.d.ts +48 -0
- package/dist/templates/auth.d.ts.map +1 -0
- package/dist/templates/auth.js +381 -0
- package/dist/templates/billing.d.ts +44 -0
- package/dist/templates/billing.d.ts.map +1 -0
- package/dist/templates/billing.js +551 -0
- package/dist/templates/chat.d.ts +63 -0
- package/dist/templates/chat.d.ts.map +1 -0
- package/dist/templates/chat.js +1979 -0
- package/dist/templates/components.d.ts +22 -0
- package/dist/templates/components.d.ts.map +1 -0
- package/dist/templates/components.js +672 -0
- package/dist/templates/config.d.ts +6 -0
- package/dist/templates/config.d.ts.map +1 -0
- package/dist/templates/config.js +86 -0
- package/dist/templates/docker.d.ts +25 -0
- package/dist/templates/docker.d.ts.map +1 -0
- package/dist/templates/docker.js +165 -0
- package/dist/templates/gdpr.d.ts +16 -0
- package/dist/templates/gdpr.d.ts.map +1 -0
- package/dist/templates/gdpr.js +259 -0
- package/dist/templates/index.d.ts +77 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +339 -0
- package/dist/templates/layout.d.ts +67 -0
- package/dist/templates/layout.d.ts.map +1 -0
- package/dist/templates/layout.js +670 -0
- package/dist/templates/mfa.d.ts +23 -0
- package/dist/templates/mfa.d.ts.map +1 -0
- package/dist/templates/mfa.js +353 -0
- package/dist/templates/middleware.d.ts +12 -0
- package/dist/templates/middleware.d.ts.map +1 -0
- package/dist/templates/middleware.js +116 -0
- package/dist/templates/prisma.d.ts +35 -0
- package/dist/templates/prisma.d.ts.map +1 -0
- package/dist/templates/prisma.js +724 -0
- package/dist/templates/provider-routes.d.ts +21 -0
- package/dist/templates/provider-routes.d.ts.map +1 -0
- package/dist/templates/provider-routes.js +1203 -0
- package/dist/templates/rag.d.ts +48 -0
- package/dist/templates/rag.d.ts.map +1 -0
- package/dist/templates/rag.js +532 -0
- package/dist/templates/widget.d.ts +64 -0
- package/dist/templates/widget.d.ts.map +1 -0
- package/dist/templates/widget.js +1360 -0
- package/dist/utils/provider-db.d.ts +63 -0
- package/dist/utils/provider-db.d.ts.map +1 -0
- package/dist/utils/provider-db.js +300 -0
- package/dist/utils.d.ts +78 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +330 -0
- package/package.json +60 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reusable component page templates
|
|
4
|
+
* Generates provider management and prompt management pages
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.generateModelProvidersPage = generateModelProvidersPage;
|
|
8
|
+
exports.generatePromptManagementPage = generatePromptManagementPage;
|
|
9
|
+
exports.generatePromptSelector = generatePromptSelector;
|
|
10
|
+
/**
|
|
11
|
+
* Generates the Model Providers management page
|
|
12
|
+
* Self-contained — no external @chimerai/* imports needed
|
|
13
|
+
* @returns TypeScript/JSX content for app/dashboard/providers/page.tsx
|
|
14
|
+
*/
|
|
15
|
+
function generateModelProvidersPage() {
|
|
16
|
+
return `// @chimerai component=ModelProvidersPage version=1.0
|
|
17
|
+
// Model Providers Management Page
|
|
18
|
+
'use client';
|
|
19
|
+
|
|
20
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
21
|
+
import { useSession } from 'next-auth/react';
|
|
22
|
+
|
|
23
|
+
interface ProviderModel {
|
|
24
|
+
id: string;
|
|
25
|
+
modelId: string;
|
|
26
|
+
name: string;
|
|
27
|
+
capabilities: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Provider {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
type: string;
|
|
34
|
+
baseUrl?: string;
|
|
35
|
+
status: string;
|
|
36
|
+
isDefault: boolean;
|
|
37
|
+
models: ProviderModel[];
|
|
38
|
+
createdAt: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ProviderFormData {
|
|
42
|
+
name: string;
|
|
43
|
+
type: string;
|
|
44
|
+
apiKey: string;
|
|
45
|
+
baseUrl: string;
|
|
46
|
+
defaultModel: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface TestResult {
|
|
50
|
+
success: boolean;
|
|
51
|
+
responseTime?: number;
|
|
52
|
+
errorMessage?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const PROVIDER_TYPES = [
|
|
56
|
+
{ value: 'openai', label: 'OpenAI', placeholder: 'https://api.openai.com/v1' },
|
|
57
|
+
{ value: 'anthropic', label: 'Anthropic', placeholder: 'https://api.anthropic.com' },
|
|
58
|
+
{ value: 'ollama', label: 'Ollama (Local)', placeholder: 'http://localhost:11434' },
|
|
59
|
+
{ value: 'groq', label: 'Groq', placeholder: 'https://api.groq.com/openai/v1' },
|
|
60
|
+
{ value: 'google', label: 'Google AI', placeholder: '' },
|
|
61
|
+
{ value: 'custom', label: 'Custom OpenAI-Compatible', placeholder: '' },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const emptyForm: ProviderFormData = { name: '', type: 'openai', apiKey: '', baseUrl: '', defaultModel: '' };
|
|
65
|
+
|
|
66
|
+
export default function ProvidersPage() {
|
|
67
|
+
const { data: session } = useSession();
|
|
68
|
+
const [providers, setProviders] = useState<Provider[]>([]);
|
|
69
|
+
const [loading, setLoading] = useState(true);
|
|
70
|
+
const [showForm, setShowForm] = useState(false);
|
|
71
|
+
const [editingId, setEditingId] = useState<string | null>(null);
|
|
72
|
+
const [formData, setFormData] = useState<ProviderFormData>(emptyForm);
|
|
73
|
+
const [saving, setSaving] = useState(false);
|
|
74
|
+
const [testing, setTesting] = useState<Record<string, boolean>>({});
|
|
75
|
+
const [testResults, setTestResults] = useState<Record<string, TestResult>>({});
|
|
76
|
+
const [error, setError] = useState<string | null>(null);
|
|
77
|
+
const [editingModels, setEditingModels] = useState<ProviderModel[]>([]);
|
|
78
|
+
|
|
79
|
+
const fetchProviders = useCallback(async () => {
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch('/api/providers');
|
|
82
|
+
if (!res.ok) throw new Error('Failed to load providers');
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
setProviders(data.providers || data || []);
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
setError(err.message);
|
|
87
|
+
} finally {
|
|
88
|
+
setLoading(false);
|
|
89
|
+
}
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
useEffect(() => { fetchProviders(); }, [fetchProviders]);
|
|
93
|
+
|
|
94
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
setSaving(true);
|
|
97
|
+
setError(null);
|
|
98
|
+
try {
|
|
99
|
+
const url = editingId ? \`/api/providers/\${editingId}\` : '/api/providers';
|
|
100
|
+
const method = editingId ? 'PUT' : 'POST';
|
|
101
|
+
const config: Record<string, string> = {};
|
|
102
|
+
if (formData.apiKey) config.apiKey = formData.apiKey;
|
|
103
|
+
if (formData.baseUrl) config.baseUrl = formData.baseUrl;
|
|
104
|
+
if (formData.defaultModel) config.defaultModel = formData.defaultModel;
|
|
105
|
+
const body: Record<string, any> = {
|
|
106
|
+
name: formData.name,
|
|
107
|
+
type: formData.type,
|
|
108
|
+
config,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
112
|
+
if (!res.ok) throw new Error('Failed to save provider');
|
|
113
|
+
setShowForm(false);
|
|
114
|
+
setEditingId(null);
|
|
115
|
+
setFormData(emptyForm);
|
|
116
|
+
await fetchProviders();
|
|
117
|
+
} catch (err: any) {
|
|
118
|
+
setError(err.message);
|
|
119
|
+
} finally {
|
|
120
|
+
setSaving(false);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleDelete = async (id: string) => {
|
|
125
|
+
if (!confirm('Delete this provider?')) return;
|
|
126
|
+
try {
|
|
127
|
+
await fetch(\`/api/providers/\${id}\`, { method: 'DELETE' });
|
|
128
|
+
await fetchProviders();
|
|
129
|
+
} catch (err: any) {
|
|
130
|
+
setError(err.message);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleTest = async (id: string) => {
|
|
135
|
+
setTesting(prev => ({ ...prev, [id]: true }));
|
|
136
|
+
try {
|
|
137
|
+
const res = await fetch(\`/api/providers/\${id}/test\`, { method: 'POST' });
|
|
138
|
+
const data = await res.json();
|
|
139
|
+
setTestResults(prev => ({ ...prev, [id]: data }));
|
|
140
|
+
// Auto-sync models after successful test
|
|
141
|
+
if (data.success) {
|
|
142
|
+
await handleSync(id);
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
setTestResults(prev => ({ ...prev, [id]: { success: false, errorMessage: 'Connection failed' } }));
|
|
146
|
+
} finally {
|
|
147
|
+
setTesting(prev => ({ ...prev, [id]: false }));
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const [syncing, setSyncing] = useState<Record<string, boolean>>({});
|
|
152
|
+
const handleSync = async (id: string) => {
|
|
153
|
+
setSyncing(prev => ({ ...prev, [id]: true }));
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(\`/api/providers/\${id}/sync\`, { method: 'POST' });
|
|
156
|
+
const data = await res.json();
|
|
157
|
+
if (data.success) {
|
|
158
|
+
await fetchProviders();
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Sync failed silently — models will appear on next manual sync
|
|
162
|
+
} finally {
|
|
163
|
+
setSyncing(prev => ({ ...prev, [id]: false }));
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const handleToggleStatus = async (id: string, currentStatus: string) => {
|
|
168
|
+
const newStatus = currentStatus === 'active' ? 'inactive' : 'active';
|
|
169
|
+
try {
|
|
170
|
+
const res = await fetch(\`/api/providers/\${id}\`, {
|
|
171
|
+
method: 'PATCH',
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
body: JSON.stringify({ status: newStatus }),
|
|
174
|
+
});
|
|
175
|
+
if (!res.ok) throw new Error('Failed to update status');
|
|
176
|
+
await fetchProviders();
|
|
177
|
+
} catch (err: any) {
|
|
178
|
+
setError(err.message);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const openEdit = (provider: Provider) => {
|
|
183
|
+
// Parse config safely (PostgreSQL returns JSON object, SQLite returns JSON string)
|
|
184
|
+
const rawConfig = (provider as any).config;
|
|
185
|
+
const providerConfig: { defaultModel?: string } | null =
|
|
186
|
+
typeof rawConfig === 'string' ? (() => { try { return JSON.parse(rawConfig); } catch { return null; } })() : rawConfig || null;
|
|
187
|
+
setFormData({ name: provider.name, type: provider.type, apiKey: '', baseUrl: provider.baseUrl || '', defaultModel: providerConfig?.defaultModel || '' });
|
|
188
|
+
setEditingModels(provider.models || []);
|
|
189
|
+
setEditingId(provider.id);
|
|
190
|
+
setShowForm(true);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (!session?.user?.id) {
|
|
194
|
+
return <div className="container mx-auto py-8"><p className="text-center text-gray-500">Please sign in to manage model providers.</p></div>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="container mx-auto py-8 max-w-5xl">
|
|
199
|
+
<div className="flex items-center justify-between mb-6">
|
|
200
|
+
<div>
|
|
201
|
+
<h1 className="text-3xl font-bold mb-1 dark:text-white">AI Model Providers</h1>
|
|
202
|
+
<p className="text-gray-600 dark:text-gray-400">Manage your AI model provider connections</p>
|
|
203
|
+
</div>
|
|
204
|
+
<button onClick={() => { setFormData(emptyForm); setEditingId(null); setEditingModels([]); setShowForm(true); }}
|
|
205
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
|
206
|
+
+ Add Provider
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{error && <div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 rounded-lg">{error}</div>}
|
|
211
|
+
|
|
212
|
+
{showForm && (
|
|
213
|
+
<div className="mb-6 p-6 border dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 shadow-sm">
|
|
214
|
+
<h2 className="text-lg font-semibold mb-4 dark:text-white">{editingId ? 'Edit' : 'Add'} Provider</h2>
|
|
215
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
216
|
+
<div className="grid grid-cols-2 gap-4">
|
|
217
|
+
<div>
|
|
218
|
+
<label className="block text-sm font-medium mb-1 dark:text-gray-300">Name</label>
|
|
219
|
+
<input type="text" required value={formData.name}
|
|
220
|
+
onChange={e => setFormData(f => ({ ...f, name: e.target.value }))}
|
|
221
|
+
className="w-full px-3 py-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white" placeholder="My OpenAI" />
|
|
222
|
+
</div>
|
|
223
|
+
<div>
|
|
224
|
+
<label className="block text-sm font-medium mb-1 dark:text-gray-300">Type</label>
|
|
225
|
+
<select value={formData.type} onChange={e => setFormData(f => ({ ...f, type: e.target.value }))}
|
|
226
|
+
className="w-full px-3 py-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white">
|
|
227
|
+
{PROVIDER_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
|
|
228
|
+
</select>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
<div>
|
|
232
|
+
<label className="block text-sm font-medium mb-1 dark:text-gray-300">API Key {editingId ? '(leave empty to keep current)' : formData.type === 'ollama' ? '(optional for Ollama)' : ''}</label>
|
|
233
|
+
<input type="password" value={formData.apiKey}
|
|
234
|
+
onChange={e => setFormData(f => ({ ...f, apiKey: e.target.value }))}
|
|
235
|
+
className="w-full px-3 py-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white" placeholder={formData.type === 'ollama' ? '(not required)' : 'sk-...'} required={!editingId && formData.type !== 'ollama'} />
|
|
236
|
+
</div>
|
|
237
|
+
<div>
|
|
238
|
+
<label className="block text-sm font-medium mb-1 dark:text-gray-300">Base URL (optional)</label>
|
|
239
|
+
<input type="text" value={formData.baseUrl}
|
|
240
|
+
onChange={e => setFormData(f => ({ ...f, baseUrl: e.target.value }))}
|
|
241
|
+
className="w-full px-3 py-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white"
|
|
242
|
+
placeholder={PROVIDER_TYPES.find(t => t.value === formData.type)?.placeholder || ''} />
|
|
243
|
+
</div>
|
|
244
|
+
<div>
|
|
245
|
+
<label className="block text-sm font-medium mb-1 dark:text-gray-300">Default Model</label>
|
|
246
|
+
{editingModels.length > 0 ? (
|
|
247
|
+
<select value={formData.defaultModel}
|
|
248
|
+
onChange={e => setFormData(f => ({ ...f, defaultModel: e.target.value }))}
|
|
249
|
+
className="w-full px-3 py-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white">
|
|
250
|
+
<option value="">— Select a model —</option>
|
|
251
|
+
{editingModels
|
|
252
|
+
.filter(m => !m.capabilities || m.capabilities.length === 0 || m.capabilities.some(c => ['chat', 'completion'].includes(c)))
|
|
253
|
+
.map(m => <option key={m.id} value={m.modelId}>{m.name || m.modelId}</option>)}
|
|
254
|
+
</select>
|
|
255
|
+
) : (
|
|
256
|
+
<input type="text" value={formData.defaultModel}
|
|
257
|
+
onChange={e => setFormData(f => ({ ...f, defaultModel: e.target.value }))}
|
|
258
|
+
className="w-full px-3 py-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white"
|
|
259
|
+
placeholder="e.g. gpt-4o-mini" />
|
|
260
|
+
)}
|
|
261
|
+
<p className="text-xs text-gray-400 mt-1">Used when the widget does not specify a model.{editingModels.length === 0 && editingId ? ' Sync models first to see available options.' : ''}</p>
|
|
262
|
+
</div>
|
|
263
|
+
<div className="flex gap-2">
|
|
264
|
+
<button type="submit" disabled={saving}
|
|
265
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">
|
|
266
|
+
{saving ? 'Saving...' : editingId ? 'Update' : 'Create'}
|
|
267
|
+
</button>
|
|
268
|
+
<button type="button" onClick={() => setShowForm(false)}
|
|
269
|
+
className="px-4 py-2 border dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 dark:text-gray-300">Cancel</button>
|
|
270
|
+
</div>
|
|
271
|
+
</form>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{loading ? (
|
|
276
|
+
<div className="space-y-4">{[1,2,3].map(i => <div key={i} className="h-32 bg-gray-100 dark:bg-gray-800 rounded-lg animate-pulse" />)}</div>
|
|
277
|
+
) : providers.length === 0 ? (
|
|
278
|
+
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
|
279
|
+
<p className="text-lg mb-2">No providers configured</p>
|
|
280
|
+
<p>Click "Add Provider" to connect your first AI model provider.</p>
|
|
281
|
+
</div>
|
|
282
|
+
) : (
|
|
283
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
284
|
+
{providers.map(provider => (
|
|
285
|
+
<div key={provider.id} className="p-5 border dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 shadow-sm">
|
|
286
|
+
<div className="flex items-center justify-between mb-3">
|
|
287
|
+
<div className="flex items-center gap-2">
|
|
288
|
+
<h3 className="font-semibold text-lg dark:text-white">{provider.name}</h3>
|
|
289
|
+
{provider.isDefault && <span className="text-xs bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 px-2 py-0.5 rounded-full">Default</span>}
|
|
290
|
+
</div>
|
|
291
|
+
<span className={\`text-xs px-2 py-0.5 rounded-full \${provider.status === 'active' ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300' : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'}\`}>
|
|
292
|
+
{provider.status}
|
|
293
|
+
</span>
|
|
294
|
+
</div>
|
|
295
|
+
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">Type: {provider.type}</p>
|
|
296
|
+
{provider.models.length > 0 && (
|
|
297
|
+
<div className="flex flex-wrap gap-1 mb-3">
|
|
298
|
+
{provider.models.slice(0, 4).map(m => (
|
|
299
|
+
<span key={m.id} className="text-xs bg-gray-100 dark:bg-gray-700 dark:text-gray-300 px-2 py-0.5 rounded">{m.name || m.modelId}</span>
|
|
300
|
+
))}
|
|
301
|
+
{provider.models.length > 4 && <span className="text-xs text-gray-400">+{provider.models.length - 4} more</span>}
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
{testResults[provider.id] && (
|
|
305
|
+
<div className={\`text-sm p-2 rounded mb-3 \${testResults[provider.id].success ? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300' : 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300'}\`}>
|
|
306
|
+
{testResults[provider.id].success ? \`✓ Connected (\${testResults[provider.id].responseTime}ms)\` : \`✗ \${testResults[provider.id].errorMessage}\`}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
<div className="flex gap-2 mt-3 pt-3 border-t dark:border-gray-700">
|
|
310
|
+
<button onClick={() => handleTest(provider.id)} disabled={testing[provider.id]}
|
|
311
|
+
className="text-sm px-3 py-1 border dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 dark:text-gray-300 disabled:opacity-50">
|
|
312
|
+
{testing[provider.id] ? 'Testing...' : 'Test'}
|
|
313
|
+
</button>
|
|
314
|
+
<button onClick={() => handleSync(provider.id)} disabled={syncing[provider.id]}
|
|
315
|
+
className="text-sm px-3 py-1 border dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 dark:text-gray-300 disabled:opacity-50">
|
|
316
|
+
{syncing[provider.id] ? 'Syncing...' : 'Sync Models'}
|
|
317
|
+
</button>
|
|
318
|
+
<button onClick={() => openEdit(provider)} className="text-sm px-3 py-1 border dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 dark:text-gray-300">Edit</button>
|
|
319
|
+
<button onClick={() => handleToggleStatus(provider.id, provider.status)}
|
|
320
|
+
className={\`text-sm px-3 py-1 border rounded \${provider.status === 'active' ? 'border-yellow-200 dark:border-yellow-800 text-yellow-600 dark:text-yellow-400 hover:bg-yellow-50 dark:hover:bg-yellow-900/20' : 'border-green-200 dark:border-green-800 text-green-600 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/20'}\`}>
|
|
321
|
+
{provider.status === 'active' ? 'Deactivate' : 'Activate'}
|
|
322
|
+
</button>
|
|
323
|
+
<button onClick={() => handleDelete(provider.id)}
|
|
324
|
+
className="text-sm px-3 py-1 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 rounded hover:bg-red-50 dark:hover:bg-red-900/20">Delete</button>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
))}
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Generates the Prompt Management page
|
|
337
|
+
* v2: PROMPT_CATEGORIES, isDefault toggle, auto-extracted variable display, shared PromptModal
|
|
338
|
+
* @returns TypeScript/JSX content for app/dashboard/prompts/page.tsx
|
|
339
|
+
*/
|
|
340
|
+
function generatePromptManagementPage() {
|
|
341
|
+
return `// @chimerai component=PromptManagementPage version=2.0
|
|
342
|
+
'use client';
|
|
343
|
+
|
|
344
|
+
import { useState, useEffect } from 'react';
|
|
345
|
+
|
|
346
|
+
const PROMPT_CATEGORIES = [
|
|
347
|
+
{ value: 'system', label: 'System Prompt', description: 'Chat conversation behavior' },
|
|
348
|
+
{ value: 'rag', label: 'RAG Template', description: 'Document context framing' },
|
|
349
|
+
{ value: 'widget', label: 'Widget Prompt', description: 'For embedded chat widget' },
|
|
350
|
+
{ value: 'user', label: 'User Template', description: 'Suggested user prompts' },
|
|
351
|
+
] as const;
|
|
352
|
+
|
|
353
|
+
interface PromptTemplate {
|
|
354
|
+
id: string;
|
|
355
|
+
name: string;
|
|
356
|
+
category: string;
|
|
357
|
+
description?: string;
|
|
358
|
+
content: string;
|
|
359
|
+
variables: string[];
|
|
360
|
+
language: string;
|
|
361
|
+
version: number;
|
|
362
|
+
isActive: boolean;
|
|
363
|
+
isDefault: boolean;
|
|
364
|
+
tags: string[];
|
|
365
|
+
createdBy: string | null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const EMPTY_FORM = { name: '', category: 'system', description: '', content: '', language: 'en' };
|
|
369
|
+
|
|
370
|
+
export default function PromptsPage() {
|
|
371
|
+
const [templates, setTemplates] = useState<PromptTemplate[]>([]);
|
|
372
|
+
const [loading, setLoading] = useState(true);
|
|
373
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
374
|
+
const [categoryFilter, setCategoryFilter] = useState('all');
|
|
375
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
376
|
+
const [showEdit, setShowEdit] = useState(false);
|
|
377
|
+
const [current, setCurrent] = useState<PromptTemplate | null>(null);
|
|
378
|
+
const [formData, setFormData] = useState(EMPTY_FORM);
|
|
379
|
+
const [error, setError] = useState('');
|
|
380
|
+
|
|
381
|
+
useEffect(() => { fetchTemplates(); }, []);
|
|
382
|
+
|
|
383
|
+
async function fetchTemplates() {
|
|
384
|
+
try {
|
|
385
|
+
const res = await fetch('/api/prompts');
|
|
386
|
+
const data = await res.json();
|
|
387
|
+
setTemplates(Array.isArray(data) ? data : []);
|
|
388
|
+
} catch { console.error('Failed to fetch templates'); }
|
|
389
|
+
finally { setLoading(false); }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function handleCreate(e: React.FormEvent) {
|
|
393
|
+
e.preventDefault();
|
|
394
|
+
setError('');
|
|
395
|
+
const res = await fetch('/api/prompts', {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
headers: { 'Content-Type': 'application/json' },
|
|
398
|
+
body: JSON.stringify(formData),
|
|
399
|
+
});
|
|
400
|
+
if (!res.ok) { setError((await res.json()).error || 'Failed to create'); return; }
|
|
401
|
+
setShowCreate(false);
|
|
402
|
+
setFormData(EMPTY_FORM);
|
|
403
|
+
fetchTemplates();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function handleUpdate(e: React.FormEvent) {
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
if (!current) return;
|
|
409
|
+
setError('');
|
|
410
|
+
const res = await fetch(\`/api/prompts/\${current.id}\`, {
|
|
411
|
+
method: 'PUT',
|
|
412
|
+
headers: { 'Content-Type': 'application/json' },
|
|
413
|
+
body: JSON.stringify(formData),
|
|
414
|
+
});
|
|
415
|
+
if (!res.ok) { setError((await res.json()).error || 'Failed to update'); return; }
|
|
416
|
+
setShowEdit(false);
|
|
417
|
+
setCurrent(null);
|
|
418
|
+
fetchTemplates();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function handleDelete(id: string) {
|
|
422
|
+
if (!confirm('Delete this template?')) return;
|
|
423
|
+
const res = await fetch(\`/api/prompts/\${id}\`, { method: 'DELETE' });
|
|
424
|
+
if (res.ok) fetchTemplates();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function handleSetDefault(id: string) {
|
|
428
|
+
const res = await fetch(\`/api/prompts/\${id}/set-default\`, { method: 'POST' });
|
|
429
|
+
if (res.ok) fetchTemplates();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function openEdit(t: PromptTemplate) {
|
|
433
|
+
setCurrent(t);
|
|
434
|
+
setFormData({ name: t.name, category: t.category, description: t.description || '', content: t.content, language: t.language });
|
|
435
|
+
setError('');
|
|
436
|
+
setShowEdit(true);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function getCategoryLabel(v: string) {
|
|
440
|
+
return PROMPT_CATEGORIES.find(c => c.value === v)?.label ?? v;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const filtered = templates.filter(t =>
|
|
444
|
+
(categoryFilter === 'all' || t.category === categoryFilter) &&
|
|
445
|
+
(!searchQuery || t.name.toLowerCase().includes(searchQuery.toLowerCase()) || t.description?.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
if (loading) return <div className="p-8 text-gray-500">Loading...</div>;
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div className="container mx-auto py-8 px-4 max-w-4xl">
|
|
452
|
+
<div className="flex justify-between items-center mb-6">
|
|
453
|
+
<div>
|
|
454
|
+
<h1 className="text-2xl font-bold">Prompt Templates</h1>
|
|
455
|
+
<p className="text-sm text-gray-500 mt-1">Manage system prompts and RAG templates for your AI</p>
|
|
456
|
+
</div>
|
|
457
|
+
<button
|
|
458
|
+
onClick={() => { setFormData(EMPTY_FORM); setError(''); setShowCreate(true); }}
|
|
459
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium"
|
|
460
|
+
>
|
|
461
|
+
+ Create Template
|
|
462
|
+
</button>
|
|
463
|
+
</div>
|
|
464
|
+
|
|
465
|
+
<div className="mb-4 flex gap-3">
|
|
466
|
+
<input
|
|
467
|
+
type="text"
|
|
468
|
+
placeholder="Search templates..."
|
|
469
|
+
value={searchQuery}
|
|
470
|
+
onChange={e => setSearchQuery(e.target.value)}
|
|
471
|
+
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|
472
|
+
/>
|
|
473
|
+
<select
|
|
474
|
+
value={categoryFilter}
|
|
475
|
+
onChange={e => setCategoryFilter(e.target.value)}
|
|
476
|
+
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|
477
|
+
>
|
|
478
|
+
<option value="all">All Categories</option>
|
|
479
|
+
{PROMPT_CATEGORIES.map(c => (
|
|
480
|
+
<option key={c.value} value={c.value}>{c.label}</option>
|
|
481
|
+
))}
|
|
482
|
+
</select>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
{filtered.length === 0 && (
|
|
486
|
+
<div className="text-center py-12 text-gray-400">No templates found.</div>
|
|
487
|
+
)}
|
|
488
|
+
|
|
489
|
+
<div className="space-y-3">
|
|
490
|
+
{filtered.map(template => (
|
|
491
|
+
<div key={template.id} className="border border-gray-200 rounded-lg p-4 bg-white hover:border-gray-300 transition-colors">
|
|
492
|
+
<div className="flex justify-between items-start gap-4">
|
|
493
|
+
<div className="flex-1 min-w-0">
|
|
494
|
+
<div className="flex items-center gap-2 flex-wrap mb-1">
|
|
495
|
+
<h3 className="font-semibold text-gray-900">{template.name}</h3>
|
|
496
|
+
<span className="px-2 py-0.5 bg-gray-100 text-gray-600 rounded text-xs">{getCategoryLabel(template.category)}</span>
|
|
497
|
+
{template.isDefault && <span className="px-2 py-0.5 bg-green-100 text-green-700 rounded text-xs font-medium">Default</span>}
|
|
498
|
+
{template.createdBy === null && <span className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-xs">System</span>}
|
|
499
|
+
</div>
|
|
500
|
+
{template.description && <p className="text-sm text-gray-500 mb-2">{template.description}</p>}
|
|
501
|
+
<p className="text-xs text-gray-400 line-clamp-2 font-mono bg-gray-50 p-2 rounded">{template.content}</p>
|
|
502
|
+
{template.variables.length > 0 && (
|
|
503
|
+
<div className="mt-2 flex items-center gap-1 flex-wrap">
|
|
504
|
+
<span className="text-xs text-gray-400">Variables:</span>
|
|
505
|
+
{template.variables.map(v => (
|
|
506
|
+
<span key={v} className="px-1.5 py-0.5 bg-yellow-50 text-yellow-700 rounded text-xs font-mono">{'{{' + v + '}}'}</span>
|
|
507
|
+
))}
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
</div>
|
|
511
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
512
|
+
{!template.isDefault && (
|
|
513
|
+
<button
|
|
514
|
+
onClick={() => handleSetDefault(template.id)}
|
|
515
|
+
className="px-2 py-1 text-xs text-gray-500 border border-gray-200 rounded hover:border-green-400 hover:text-green-600 transition-colors"
|
|
516
|
+
title="Set as default for this category"
|
|
517
|
+
>
|
|
518
|
+
Set Default
|
|
519
|
+
</button>
|
|
520
|
+
)}
|
|
521
|
+
{template.createdBy !== null && (
|
|
522
|
+
<>
|
|
523
|
+
<button onClick={() => openEdit(template)} className="px-2 py-1 text-xs text-blue-600 border border-blue-200 rounded hover:bg-blue-50">Edit</button>
|
|
524
|
+
<button onClick={() => handleDelete(template.id)} className="px-2 py-1 text-xs text-red-600 border border-red-200 rounded hover:bg-red-50">Delete</button>
|
|
525
|
+
</>
|
|
526
|
+
)}
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
))}
|
|
531
|
+
</div>
|
|
532
|
+
|
|
533
|
+
{showCreate && (
|
|
534
|
+
<PromptModal
|
|
535
|
+
title="Create Prompt Template"
|
|
536
|
+
formData={formData}
|
|
537
|
+
setFormData={setFormData}
|
|
538
|
+
onSubmit={handleCreate}
|
|
539
|
+
onCancel={() => setShowCreate(false)}
|
|
540
|
+
submitLabel="Create"
|
|
541
|
+
error={error}
|
|
542
|
+
/>
|
|
543
|
+
)}
|
|
544
|
+
{showEdit && (
|
|
545
|
+
<PromptModal
|
|
546
|
+
title="Edit Prompt Template"
|
|
547
|
+
formData={formData}
|
|
548
|
+
setFormData={setFormData}
|
|
549
|
+
onSubmit={handleUpdate}
|
|
550
|
+
onCancel={() => { setShowEdit(false); setCurrent(null); }}
|
|
551
|
+
submitLabel="Save Changes"
|
|
552
|
+
error={error}
|
|
553
|
+
/>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function PromptModal({
|
|
560
|
+
title, formData, setFormData, onSubmit, onCancel, submitLabel, error,
|
|
561
|
+
}: {
|
|
562
|
+
title: string;
|
|
563
|
+
formData: any;
|
|
564
|
+
setFormData: (d: any) => void;
|
|
565
|
+
onSubmit: (e: React.FormEvent) => void;
|
|
566
|
+
onCancel: () => void;
|
|
567
|
+
submitLabel: string;
|
|
568
|
+
error: string;
|
|
569
|
+
}) {
|
|
570
|
+
return (
|
|
571
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
572
|
+
<div className="bg-white rounded-xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
573
|
+
<h2 className="text-xl font-bold mb-4">{title}</h2>
|
|
574
|
+
{error && <div className="mb-4 p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>}
|
|
575
|
+
<form onSubmit={onSubmit} className="space-y-4">
|
|
576
|
+
<div>
|
|
577
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
578
|
+
<input type="text" value={formData.name} onChange={e => setFormData({ ...formData, name: e.target.value })} required placeholder="e.g. Customer Support Agent" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm" />
|
|
579
|
+
</div>
|
|
580
|
+
<div>
|
|
581
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
|
582
|
+
<select value={formData.category} onChange={e => setFormData({ ...formData, category: e.target.value })} required className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
|
583
|
+
{PROMPT_CATEGORIES.map(c => <option key={c.value} value={c.value}>{c.label} - {c.description}</option>)}
|
|
584
|
+
</select>
|
|
585
|
+
</div>
|
|
586
|
+
<div>
|
|
587
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
588
|
+
<input type="text" value={formData.description} onChange={e => setFormData({ ...formData, description: e.target.value })} placeholder="Brief description" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm" />
|
|
589
|
+
</div>
|
|
590
|
+
<div>
|
|
591
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
592
|
+
Content
|
|
593
|
+
<span className="ml-1 text-xs text-gray-400 font-normal">Use {'{{variable}}'} for dynamic placeholders</span>
|
|
594
|
+
</label>
|
|
595
|
+
<textarea value={formData.content} onChange={e => setFormData({ ...formData, content: e.target.value })} required rows={6} placeholder="You are a helpful assistant..." className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm font-mono" />
|
|
596
|
+
</div>
|
|
597
|
+
<div className="flex gap-2 pt-2">
|
|
598
|
+
<button type="submit" className="flex-1 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium">{submitLabel}</button>
|
|
599
|
+
<button type="button" onClick={onCancel} className="flex-1 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-sm">Cancel</button>
|
|
600
|
+
</div>
|
|
601
|
+
</form>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
`;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Generates the PromptSelector dropdown component for use in the chat UI
|
|
610
|
+
* @returns TypeScript/JSX content for components/chat/prompt-selector.tsx
|
|
611
|
+
*/
|
|
612
|
+
function generatePromptSelector() {
|
|
613
|
+
return `// @chimerai component=PromptSelector version=1.0
|
|
614
|
+
'use client';
|
|
615
|
+
|
|
616
|
+
import { useState, useEffect } from 'react';
|
|
617
|
+
|
|
618
|
+
interface PromptOption {
|
|
619
|
+
id: string;
|
|
620
|
+
name: string;
|
|
621
|
+
isDefault: boolean;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
interface PromptSelectorProps {
|
|
625
|
+
value: string | null;
|
|
626
|
+
onChange: (promptId: string | null) => void;
|
|
627
|
+
/** Filter to this category (default: 'system') */
|
|
628
|
+
category?: string;
|
|
629
|
+
placeholder?: string;
|
|
630
|
+
className?: string;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Dropdown to select a prompt template for a chat conversation.
|
|
635
|
+
* Usage: <PromptSelector value={promptId} onChange={setPromptId} category="system" />
|
|
636
|
+
*/
|
|
637
|
+
export function PromptSelector({
|
|
638
|
+
value,
|
|
639
|
+
onChange,
|
|
640
|
+
category = 'system',
|
|
641
|
+
placeholder = 'No system prompt',
|
|
642
|
+
className = '',
|
|
643
|
+
}: PromptSelectorProps) {
|
|
644
|
+
const [prompts, setPrompts] = useState<PromptOption[]>([]);
|
|
645
|
+
const [loading, setLoading] = useState(true);
|
|
646
|
+
|
|
647
|
+
useEffect(() => {
|
|
648
|
+
fetch(\`/api/prompts?category=\${category}\`)
|
|
649
|
+
.then(r => r.json())
|
|
650
|
+
.then(data => setPrompts(Array.isArray(data) ? data : []))
|
|
651
|
+
.catch(() => setPrompts([]))
|
|
652
|
+
.finally(() => setLoading(false));
|
|
653
|
+
}, [category]);
|
|
654
|
+
|
|
655
|
+
return (
|
|
656
|
+
<select
|
|
657
|
+
value={value || ''}
|
|
658
|
+
onChange={e => onChange(e.target.value || null)}
|
|
659
|
+
disabled={loading}
|
|
660
|
+
className={\`px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white disabled:opacity-50 \${className}\`}
|
|
661
|
+
>
|
|
662
|
+
<option value="">{placeholder}</option>
|
|
663
|
+
{prompts.map(p => (
|
|
664
|
+
<option key={p.id} value={p.id}>
|
|
665
|
+
{p.name}{p.isDefault ? ' (Default)' : ''}
|
|
666
|
+
</option>
|
|
667
|
+
))}
|
|
668
|
+
</select>
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
`;
|
|
672
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function generateNextConfig(): string;
|
|
2
|
+
export declare function generateTsConfig(): string;
|
|
3
|
+
export declare function generateTailwindConfig(): string;
|
|
4
|
+
export declare function generatePostcssConfig(): string;
|
|
5
|
+
export declare function generateGlobalsCss(): string;
|
|
6
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/templates/config.ts"],"names":[],"mappings":"AAAA,wBAAgB,kBAAkB,IAAI,MAAM,CAS3C;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CA6BzC;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAgB/C;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAQ9C;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAY3C"}
|