promptlineapp 1.4.0 → 1.5.0

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.
Files changed (3) hide show
  1. package/README.md +46 -108
  2. package/bin/cli.js +422 -177
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,136 +1,74 @@
1
- # promptlineapp
1
+ # create-promptline-app
2
2
 
3
- Create AI-powered applications with [PromptLine](https://promptlineops.com) - no configuration needed.
3
+ Create AI-powered apps for the [PromptLine Marketplace](https://promptlineops.com).
4
4
 
5
5
  ## Quick Start
6
6
 
7
7
  ```bash
8
- npx promptlineapp my-app
8
+ npx create-promptline-app my-app
9
9
  cd my-app
10
+ npm run dev
10
11
  ```
11
12
 
12
- That's it! You now have a fully-configured PromptLine package ready for development.
13
+ ## Local Development
13
14
 
14
- ## Usage
15
+ ### 1. Configure your endpoints
15
16
 
16
- ### Interactive Mode (Recommended)
17
+ Open **http://localhost:5173/_dev** and add your PromptLine endpoints:
17
18
 
18
- ```bash
19
- npx promptlineapp my-app
20
- ```
21
-
22
- You'll be prompted for:
23
- - Display name
24
- - Description
25
- - Brand color
26
- - Contact email
27
- - Template preset
28
-
29
- ### Non-Interactive Mode
30
-
31
- ```bash
32
- npx promptlineapp my-app --yes
33
- npx promptlineapp my-app --preset full-app -y
34
- ```
19
+ - **Alias**: name used in code (e.g., `chatbot`, `traduction`)
20
+ - **URL**: endpoint URL from Creator Studio
21
+ - **API Key**: your API key (`sk_org_...` or `sk_ws_...`)
35
22
 
36
- ## Template Presets
23
+ Click "Check Health" to verify, then "Add Endpoint".
37
24
 
38
- | Preset | Description | Best For |
39
- |--------|-------------|----------|
40
- | `full-app` | Complete app with landing, dashboard, settings & AI | SaaS products, internal tools (default) |
41
- | `api` | Minimal frontend, backend-focused | API services, integrations |
25
+ ### 2. Use in your code
42
26
 
43
- ```bash
44
- npx promptlineapp my-app --preset full-app
45
- npx promptlineapp my-api --preset api
46
- ```
47
-
48
- ## What You Get
49
-
50
- ```
51
- my-app/
52
- ├── public/ # Public pages (no auth required)
53
- │ └── index.tsx # Landing page
54
- ├── private/ # Protected pages (auth required)
55
- │ ├── dashboard.tsx # Dashboard
56
- │ └── settings.tsx # Settings
57
- ├── backend/ # Custom API endpoints (FastAPI)
58
- │ └── api.py
59
- ├── assets/ # Static files
60
- ├── promptline.yaml # Package configuration
61
- ├── README.md
62
- └── .gitignore
63
- ```
64
-
65
- ## Configuration
66
-
67
- Your app is configured via `promptline.yaml`:
68
-
69
- ```yaml
70
- name: "My App"
71
- version: "1.0.0"
72
-
73
- # Connect AI prompts/agents
74
- ai_sources:
75
- main:
76
- type: prompt
77
- source_id: null # Set in Creator Studio
78
-
79
- # Define data models
80
- collections:
81
- submissions:
82
- fields:
83
- - name: email
84
- type: email
85
- required: true
86
-
87
- # Instance variables
88
- variables:
89
- app_name:
90
- type: string
91
- default: "My App"
92
- ```
93
-
94
- ## SDK Usage
95
-
96
- In your React pages:
97
-
98
- ```tsx
27
+ ```jsx
99
28
  import { usePromptLine } from '@promptline/sdk'
100
29
 
101
- export default function MyPage() {
102
- const { config, submitForm, callAI, user } = usePromptLine()
103
-
104
- // Access instance config
105
- const appName = config.app_name
30
+ function MyComponent() {
31
+ const { execute, isConfigured } = usePromptLine('chatbot')
106
32
 
107
- // Submit data
108
- await submitForm('submissions', { email: 'user@example.com' })
33
+ const handleClick = async () => {
34
+ const result = await execute({ text: 'Hello!' })
35
+ console.log(result.response)
36
+ }
109
37
 
110
- // Call AI
111
- const result = await callAI('main', { text: 'Hello!' })
38
+ if (!isConfigured) {
39
+ return <p>Configure 'chatbot' at <a href="/_dev">/_dev</a></p>
40
+ }
112
41
 
113
- return <h1 style={{ color: config.primary_color }}>{appName}</h1>
42
+ return <button onClick={handleClick}>Ask AI</button>
114
43
  }
115
44
  ```
116
45
 
117
- ## Deployment
46
+ ### 3. Multiple endpoints
118
47
 
119
- 1. **Upload** - Zip your package and upload to [PromptLine Creator Studio](https://app.promptlineops.com/apps/creator)
120
- 2. **Configure** - Connect your AI sources and set variables
121
- 3. **Publish** - Deploy to your users or the App Store
48
+ ```jsx
49
+ const { execute: chat } = usePromptLine('chatbot')
50
+ const { execute: translate } = usePromptLine('traduction')
122
51
 
123
- ## Documentation
124
-
125
- - [Package Development Guide](https://docs.promptlineops.com/packages)
126
- - [SDK Reference](https://docs.promptlineops.com/sdk)
127
- - [API Endpoints](https://docs.promptlineops.com/packages/backend)
52
+ // Use them independently
53
+ const chatResult = await chat({ text: 'Hello' })
54
+ const translatedResult = await translate({ text: 'Bonjour', target: 'en' })
55
+ ```
128
56
 
129
- ## Requirements
57
+ ## Project Structure
130
58
 
131
- - Node.js 16+
132
- - npm or yarn
59
+ ```
60
+ my-app/
61
+ ├── public/ # Public pages
62
+ ├── private/ # Auth-protected pages
63
+ ├── backend/ # Custom API (FastAPI)
64
+ ├── dev/ # Local dev tools (not deployed)
65
+ │ ├── sdk-mock.jsx # SDK for local testing
66
+ │ └── dev-admin.jsx # /_dev page
67
+ └── promptline.yaml # App configuration
68
+ ```
133
69
 
134
- ## License
70
+ ## Deployment
135
71
 
136
- MIT - [PromptLine](https://promptlineops.com)
72
+ 1. Click "Download Manifest" in `/_dev` to get `promptline.config.json`
73
+ 2. Upload your app to [Creator Studio](https://app.promptlineops.com)
74
+ 3. Bind your endpoints and publish
package/bin/cli.js CHANGED
@@ -1066,6 +1066,9 @@ Thumbs.db
1066
1066
  import react from '@vitejs/plugin-react'
1067
1067
  import path from 'path'
1068
1068
 
1069
+ // Internal PromptLine dev mode: set PROMPTLINE_LOCAL_DEV=true to enable local API
1070
+ const isLocalDev = process.env.PROMPTLINE_LOCAL_DEV === 'true'
1071
+
1069
1072
  export default defineConfig({
1070
1073
  plugins: [react()],
1071
1074
  resolve: {
@@ -1076,16 +1079,19 @@ export default defineConfig({
1076
1079
  server: {
1077
1080
  port: 5173,
1078
1081
  host: 'localhost',
1079
- open: true,
1082
+ open: '/_dev',
1080
1083
  strictPort: false,
1081
1084
  proxy: {
1082
- // Proxy API calls to avoid CORS issues in development
1083
- '/api/promptline-local': {
1084
- target: 'https://app.local.promptlineops.com',
1085
- changeOrigin: true,
1086
- rewrite: (path) => path.replace(/^\\/api\\/promptline-local/, '/api/v1/live'),
1087
- secure: true
1088
- },
1085
+ // Local API first (more specific path - requires PROMPTLINE_LOCAL_DEV=true)
1086
+ ...(isLocalDev ? {
1087
+ '/api/promptline-local': {
1088
+ target: 'https://app.local.promptlineops.com',
1089
+ changeOrigin: true,
1090
+ rewrite: (path) => path.replace(/^\\/api\\/promptline-local/, '/api/v1/live'),
1091
+ secure: false
1092
+ }
1093
+ } : {}),
1094
+ // Production API (marketplace developers)
1089
1095
  '/api/promptline': {
1090
1096
  target: 'https://app.promptlineops.com',
1091
1097
  changeOrigin: true,
@@ -1097,7 +1103,7 @@ export default defineConfig({
1097
1103
  })
1098
1104
  `,
1099
1105
 
1100
- // SDK mock for local development with real API support
1106
+ // SDK mock for local development with multi-endpoint support
1101
1107
  sdkMock: (config) => {
1102
1108
  const mockConfig = {
1103
1109
  app_name: config.displayName,
@@ -1107,10 +1113,16 @@ export default defineConfig({
1107
1113
  return `/**
1108
1114
  * PromptLine SDK Mock for Local Development
1109
1115
  * Supports real API calls when configured via /_dev page
1116
+ *
1117
+ * Usage:
1118
+ * const { execute } = usePromptLine('chatbot')
1119
+ * const result = await execute({ text: 'Hello!' })
1110
1120
  */
1111
- import React, { createContext, useContext, useState } from 'react'
1121
+ import React, { createContext, useContext, useState, useMemo } from 'react'
1112
1122
  import { Link as RouterLink } from 'react-router-dom'
1113
1123
 
1124
+ const ENDPOINTS_STORAGE_KEY = 'promptline_dev_endpoints'
1125
+
1114
1126
  const mockConfig = ${JSON.stringify(mockConfig, null, 2)}
1115
1127
 
1116
1128
  const mockUser = {
@@ -1119,21 +1131,39 @@ const mockUser = {
1119
1131
  name: 'Dev User'
1120
1132
  }
1121
1133
 
1122
- // Get PromptLine API config from localStorage
1123
- function getAPIConfig() {
1134
+ // Get all configured endpoints from localStorage
1135
+ function getEndpoints() {
1124
1136
  try {
1125
- return JSON.parse(localStorage.getItem('promptline_dev_config') || 'null')
1126
- } catch { return null }
1137
+ return JSON.parse(localStorage.getItem(ENDPOINTS_STORAGE_KEY) || '[]')
1138
+ } catch { return [] }
1127
1139
  }
1128
1140
 
1129
- // Call PromptLine API (sync mode)
1130
- async function callPromptLineAPI(config, input) {
1131
- const { endpoint, apiKey } = config
1141
+ // Get a specific endpoint by alias
1142
+ function getEndpoint(alias) {
1143
+ const endpoints = getEndpoints()
1144
+ return endpoints.find(ep => ep.alias === alias)
1145
+ }
1132
1146
 
1133
- // Add ?mode=sync for synchronous response
1134
- const url = endpoint.includes('?') ? endpoint + '&mode=sync' : endpoint + '?mode=sync'
1147
+ // Convert PromptLine URLs to use local proxy
1148
+ function toProxyUrl(url) {
1149
+ const localMatch = url.match(/https?:\\/\\/app\\.local\\.promptlineops\\.com\\/api\\/v1\\/live\\/([^?]+)/)
1150
+ if (localMatch) {
1151
+ return '/api/promptline-local/' + localMatch[1]
1152
+ }
1153
+ const prodMatch = url.match(/https?:\\/\\/app\\.promptlineops\\.com\\/api\\/v1\\/live\\/([^?]+)/)
1154
+ if (prodMatch) {
1155
+ return '/api/promptline/' + prodMatch[1]
1156
+ }
1157
+ return url
1158
+ }
1159
+
1160
+ // Call PromptLine API (sync mode)
1161
+ async function callPromptLineAPI(endpoint, input, variables = {}) {
1162
+ const { url, apiKey } = endpoint
1163
+ const proxyUrl = toProxyUrl(url)
1164
+ const fullUrl = proxyUrl + '?mode=sync'
1135
1165
 
1136
- const res = await fetch(url, {
1166
+ const res = await fetch(fullUrl, {
1137
1167
  method: 'POST',
1138
1168
  headers: {
1139
1169
  'Content-Type': 'application/json',
@@ -1141,25 +1171,28 @@ async function callPromptLineAPI(config, input) {
1141
1171
  },
1142
1172
  body: JSON.stringify({
1143
1173
  input: typeof input === 'string' ? { text: input } : input,
1144
- variables: {}
1174
+ variables
1145
1175
  })
1146
1176
  })
1177
+
1147
1178
  const data = await res.json()
1148
- if (!res.ok || data.error) throw new Error(data.detail || data.error?.message || data.error || res.statusText)
1149
- // Sync mode returns data.data.output or data.data.response
1179
+ if (!res.ok || data.error) {
1180
+ throw new Error(data.detail || data.error?.message || data.error || res.statusText)
1181
+ }
1182
+
1150
1183
  const output = data.data?.output || data.data?.response || data.output || data.response
1151
- return { response: typeof output === 'string' ? output : JSON.stringify(output) }
1184
+ return {
1185
+ response: typeof output === 'string' ? output : JSON.stringify(output),
1186
+ data: data.data || data
1187
+ }
1152
1188
  }
1153
1189
 
1154
1190
  const PromptLineContext = createContext(null)
1155
1191
 
1156
1192
  export function PromptLineProvider({ children }) {
1157
- const [isLoading, setIsLoading] = useState(false)
1158
-
1159
1193
  const value = {
1160
1194
  config: mockConfig,
1161
1195
  user: mockUser,
1162
- isLoading,
1163
1196
  submitForm: async (collection, data) => {
1164
1197
  console.log('[PromptLine SDK] submitForm:', collection, data)
1165
1198
  alert('Form submitted to "' + collection + '"\\n\\nData: ' + JSON.stringify(data, null, 2))
@@ -1168,23 +1201,6 @@ export function PromptLineProvider({ children }) {
1168
1201
  fetchCollection: async (collection, query) => {
1169
1202
  console.log('[PromptLine SDK] fetchCollection:', collection, query)
1170
1203
  return { items: [], total: 0 }
1171
- },
1172
- callAI: async (sourceId, input) => {
1173
- console.log('[PromptLine SDK] callAI:', sourceId, input)
1174
- const config = getAPIConfig()
1175
-
1176
- if (config && config.endpoint) {
1177
- console.log('[PromptLine SDK] Using PromptLine API:', config.endpoint)
1178
- try {
1179
- return await callPromptLineAPI(config, input)
1180
- } catch (err) {
1181
- console.error('[PromptLine SDK] API Error:', err)
1182
- return { response: '[API Error] ' + err.message, error: true }
1183
- }
1184
- }
1185
-
1186
- console.log('[PromptLine SDK] No config found, using mock. Configure at /_dev')
1187
- return { response: '[Mock] Configure your PromptLine endpoint at /_dev' }
1188
1204
  }
1189
1205
  }
1190
1206
 
@@ -1195,48 +1211,105 @@ export function PromptLineProvider({ children }) {
1195
1211
  )
1196
1212
  }
1197
1213
 
1198
- export function usePromptLine() {
1214
+ export function usePromptLine(alias) {
1199
1215
  const context = useContext(PromptLineContext)
1200
- if (!context) {
1201
- return {
1202
- config: mockConfig,
1203
- user: mockUser,
1204
- isLoading: false,
1205
- submitForm: async (c, d) => { console.log('[PromptLine SDK] submitForm:', c, d); return { success: true } },
1206
- fetchCollection: async (c, q) => { console.log('[PromptLine SDK] fetchCollection:', c, q); return { items: [], total: 0 } },
1207
- callAI: async (s, i) => { console.log('[PromptLine SDK] callAI:', s, i); return { response: '[Mock] Configure at /_dev' } }
1216
+
1217
+ const endpointData = useMemo(() => {
1218
+ if (!alias) return null
1219
+ return getEndpoint(alias)
1220
+ }, [alias])
1221
+
1222
+ const [isLoading, setIsLoading] = useState(false)
1223
+
1224
+ const execute = async (input, variables = {}) => {
1225
+ if (!endpointData) {
1226
+ console.warn('[PromptLine SDK] Endpoint "' + alias + '" not configured. Go to /_dev to configure it.')
1227
+ return {
1228
+ response: '[Not Configured] Endpoint "' + alias + '" not found. Configure it at /_dev',
1229
+ error: true
1230
+ }
1231
+ }
1232
+
1233
+ console.log('[PromptLine SDK] Calling endpoint "' + alias + '":', endpointData.url)
1234
+ setIsLoading(true)
1235
+
1236
+ try {
1237
+ const result = await callPromptLineAPI(endpointData, input, variables)
1238
+ return result
1239
+ } catch (err) {
1240
+ console.error('[PromptLine SDK] Error calling "' + alias + '":', err)
1241
+ return { response: '[API Error] ' + err.message, error: true }
1242
+ } finally {
1243
+ setIsLoading(false)
1244
+ }
1245
+ }
1246
+
1247
+ return {
1248
+ execute,
1249
+ isLoading,
1250
+ isConfigured: !!endpointData,
1251
+ endpointInfo: endpointData?.info || null,
1252
+ config: context?.config || mockConfig,
1253
+ user: context?.user || mockUser,
1254
+ submitForm: context?.submitForm || (async () => ({ success: true })),
1255
+ fetchCollection: context?.fetchCollection || (async () => ({ items: [], total: 0 })),
1256
+ callAI: async (sourceIdOrInput, inputOrVariables) => {
1257
+ let targetAlias = alias
1258
+ let input = sourceIdOrInput
1259
+ if (typeof sourceIdOrInput === 'string' && typeof inputOrVariables === 'object') {
1260
+ targetAlias = sourceIdOrInput
1261
+ input = inputOrVariables
1262
+ }
1263
+ const ep = getEndpoint(targetAlias) || getEndpoints()[0]
1264
+ if (!ep) {
1265
+ return { response: '[Not Configured] No endpoints configured. Go to /_dev', error: true }
1266
+ }
1267
+ try {
1268
+ return await callPromptLineAPI(ep, input)
1269
+ } catch (err) {
1270
+ return { response: '[API Error] ' + err.message, error: true }
1271
+ }
1208
1272
  }
1209
1273
  }
1210
- return context
1274
+ }
1275
+
1276
+ export function usePromptLineEndpoints() {
1277
+ return useMemo(() => getEndpoints(), [])
1211
1278
  }
1212
1279
 
1213
1280
  export const Link = ({ to, children, ...props }) => (
1214
1281
  <RouterLink to={to} {...props}>{children}</RouterLink>
1215
1282
  )
1216
1283
 
1217
- export default { usePromptLine, PromptLineProvider, Link }
1284
+ export default { usePromptLine, usePromptLineEndpoints, PromptLineProvider, Link }
1218
1285
  `},
1219
1286
 
1220
- // Dev admin page for configuring PromptLine endpoint
1287
+ // Dev admin page for configuring PromptLine endpoints (multi-endpoint)
1221
1288
  devAdmin: () => `/**
1222
- * PromptLine Dev Admin - Endpoint Configuration
1289
+ * PromptLine Dev Admin - Multi-Endpoint Configuration
1223
1290
  *
1224
1291
  * This page is ONLY for local development.
1225
- * Configure your PromptLine endpoint here to test with real API.
1292
+ * Configure your PromptLine endpoints here to test with real API.
1226
1293
  *
1227
1294
  * NEVER commit API keys to git!
1228
1295
  */
1229
1296
  import React, { useState, useEffect } from 'react'
1230
1297
  import { Link } from 'react-router-dom'
1231
1298
 
1299
+ const STORAGE_KEY = 'promptline_dev_endpoints'
1300
+
1232
1301
  export default function DevAdminPage() {
1233
- const [endpoint, setEndpoint] = useState('')
1234
- const [apiKey, setApiKey] = useState('')
1235
- const [saved, setSaved] = useState(false)
1302
+ const [endpoints, setEndpoints] = useState([])
1303
+ const [formAlias, setFormAlias] = useState('')
1304
+ const [formUrl, setFormUrl] = useState('')
1305
+ const [formApiKey, setFormApiKey] = useState('')
1306
+ const [formInfo, setFormInfo] = useState(null)
1307
+ const [formStatus, setFormStatus] = useState('')
1308
+ const [editingIndex, setEditingIndex] = useState(-1)
1309
+ const [selectedEndpoint, setSelectedEndpoint] = useState('')
1236
1310
  const [testInput, setTestInput] = useState('Hello! What is 2+2?')
1237
1311
  const [testResult, setTestResult] = useState('')
1238
1312
  const [testing, setTesting] = useState(false)
1239
- const [status, setStatus] = useState('') // 'connected' | 'error' | 'testing' | ''
1240
1313
  const [logs, setLogs] = useState([])
1241
1314
 
1242
1315
  const addLog = (type, message, details = null) => {
@@ -1245,114 +1318,145 @@ export default function DevAdminPage() {
1245
1318
  }
1246
1319
 
1247
1320
  useEffect(() => {
1248
- const config = JSON.parse(localStorage.getItem('promptline_dev_config') || 'null')
1249
- if (config) {
1250
- setEndpoint(config.endpoint || '')
1251
- setApiKey(config.apiKey || '')
1252
- setSaved(true)
1253
- }
1321
+ const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
1322
+ setEndpoints(saved)
1323
+ if (saved.length > 0) setSelectedEndpoint(saved[0].alias)
1254
1324
  }, [])
1255
1325
 
1256
- const saveConfig = () => {
1257
- if (!endpoint) return
1258
- const config = { endpoint, apiKey }
1259
- localStorage.setItem('promptline_dev_config', JSON.stringify(config))
1260
- setSaved(true)
1261
- addLog('info', 'Configuration saved')
1326
+ const saveEndpoints = (newEndpoints) => {
1327
+ setEndpoints(newEndpoints)
1328
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(newEndpoints))
1329
+ generateManifest(newEndpoints)
1330
+ addLog('info', 'Saved ' + newEndpoints.length + ' endpoint(s)')
1262
1331
  }
1263
1332
 
1264
- const clearConfig = () => {
1265
- localStorage.removeItem('promptline_dev_config')
1266
- setEndpoint('')
1267
- setApiKey('')
1268
- setSaved(false)
1269
- setStatus('')
1270
- addLog('info', 'Configuration cleared')
1333
+ const generateManifest = (eps) => {
1334
+ const manifest = { name: 'my-promptline-app', version: '1.0.0', endpoints: {} }
1335
+ eps.forEach(ep => {
1336
+ manifest.endpoints[ep.alias] = {
1337
+ name: ep.info?.name || ep.alias,
1338
+ description: ep.info?.description || '',
1339
+ type: ep.info?.endpoint_type || 'prompt',
1340
+ required_variables: ep.info?.required_variables || []
1341
+ }
1342
+ })
1343
+ localStorage.setItem('promptline_manifest', JSON.stringify(manifest, null, 2))
1271
1344
  }
1272
1345
 
1273
- // Convert PromptLine URLs to use local proxy (avoids CORS in development)
1274
1346
  const toProxyUrl = (url) => {
1275
- // Match app.local.promptlineops.com (local dev environment)
1276
1347
  const localMatch = url.match(/https?:\\/\\/app\\.local\\.promptlineops\\.com\\/api\\/v1\\/live\\/([^?]+)/)
1277
- if (localMatch) {
1278
- return '/api/promptline-local/' + localMatch[1]
1279
- }
1280
- // Match app.promptlineops.com (production environment)
1348
+ if (localMatch) return '/api/promptline-local/' + localMatch[1]
1281
1349
  const prodMatch = url.match(/https?:\\/\\/app\\.promptlineops\\.com\\/api\\/v1\\/live\\/([^?]+)/)
1282
- if (prodMatch) {
1283
- return '/api/promptline/' + prodMatch[1]
1284
- }
1285
- return url // Non-PromptLine URLs pass through unchanged
1350
+ if (prodMatch) return '/api/promptline/' + prodMatch[1]
1351
+ return url
1286
1352
  }
1287
1353
 
1288
- const testConnection = async () => {
1289
- if (!endpoint) return
1290
- setStatus('testing')
1291
- const proxyUrl = toProxyUrl(endpoint)
1292
- const url = proxyUrl.includes('?') ? proxyUrl + '&mode=sync' : proxyUrl + '?mode=sync'
1293
- addLog('info', 'Testing connection...', { endpoint: url, original: endpoint })
1294
-
1295
- const startTime = Date.now()
1354
+ const checkHealth = async () => {
1355
+ if (!formUrl || !formApiKey) return
1356
+ setFormStatus('checking')
1357
+ const proxyUrl = toProxyUrl(formUrl)
1358
+ addLog('info', 'Checking health: ' + (formAlias || 'new endpoint'))
1296
1359
  try {
1297
- const res = await fetch(url, {
1298
- method: 'POST',
1299
- headers: {
1300
- 'Content-Type': 'application/json',
1301
- ...(apiKey ? { 'X-API-Key': apiKey } : {})
1302
- },
1303
- body: JSON.stringify({ input: { text: 'test' }, variables: {} })
1304
- })
1305
- const duration = Date.now() - startTime
1360
+ const res = await fetch(proxyUrl + '/isAlive', { method: 'GET', headers: { 'X-API-Key': formApiKey } })
1361
+ const data = await res.json().catch(() => ({}))
1362
+ if (res.ok && data.alive === true) {
1363
+ setFormStatus('valid')
1364
+ addLog('success', 'Endpoint is alive')
1365
+ } else {
1366
+ setFormStatus('error')
1367
+ addLog('error', 'Health check failed: ' + res.status, data)
1368
+ }
1369
+ } catch (err) {
1370
+ setFormStatus('error')
1371
+ addLog('error', 'Network error', { message: err.message })
1372
+ }
1373
+ }
1306
1374
 
1375
+ const fetchInfo = async () => {
1376
+ if (!formUrl || !formApiKey) return
1377
+ const proxyUrl = toProxyUrl(formUrl)
1378
+ addLog('info', 'Fetching info: ' + (formAlias || 'new endpoint'))
1379
+ try {
1380
+ const res = await fetch(proxyUrl + '/info', { method: 'GET', headers: { 'X-API-Key': formApiKey } })
1381
+ const data = await res.json().catch(() => ({}))
1307
1382
  if (res.ok) {
1308
- setStatus('connected')
1309
- addLog('success', 'Connected (' + duration + 'ms)', { status: res.status })
1383
+ setFormInfo(data)
1384
+ addLog('success', 'Info retrieved', data)
1310
1385
  } else {
1311
- const data = await res.json().catch(() => ({}))
1312
- setStatus('error')
1313
- addLog('error', res.status + ' ' + res.statusText + ' (' + duration + 'ms)', data)
1386
+ addLog('error', 'Info fetch failed: ' + res.status, data)
1314
1387
  }
1315
1388
  } catch (err) {
1316
- const duration = Date.now() - startTime
1317
- setStatus('error')
1318
- addLog('error', 'Network error (' + duration + 'ms)', {
1319
- message: err.message,
1320
- hint: err.message.includes('Failed to fetch') ? 'CORS issue - server needs Access-Control-Allow-Origin header' : null
1321
- })
1389
+ addLog('error', 'Network error', { message: err.message })
1390
+ }
1391
+ }
1392
+
1393
+ const saveEndpoint = () => {
1394
+ if (!formAlias || !formUrl) {
1395
+ addLog('error', 'Alias and URL are required')
1396
+ return
1397
+ }
1398
+ const existingIndex = endpoints.findIndex(ep => ep.alias === formAlias)
1399
+ if (existingIndex !== -1 && existingIndex !== editingIndex) {
1400
+ addLog('error', 'Alias "' + formAlias + '" already exists')
1401
+ return
1322
1402
  }
1403
+ const newEndpoint = { alias: formAlias, url: formUrl, apiKey: formApiKey, info: formInfo, status: formStatus === 'valid' ? 'valid' : 'unchecked' }
1404
+ let newEndpoints = editingIndex >= 0 ? [...endpoints] : [...endpoints, newEndpoint]
1405
+ if (editingIndex >= 0) newEndpoints[editingIndex] = newEndpoint
1406
+ saveEndpoints(newEndpoints)
1407
+ clearForm()
1408
+ if (!selectedEndpoint) setSelectedEndpoint(formAlias)
1409
+ }
1410
+
1411
+ const editEndpoint = (index) => {
1412
+ const ep = endpoints[index]
1413
+ setFormAlias(ep.alias)
1414
+ setFormUrl(ep.url)
1415
+ setFormApiKey(ep.apiKey)
1416
+ setFormInfo(ep.info)
1417
+ setFormStatus(ep.status === 'valid' ? 'valid' : '')
1418
+ setEditingIndex(index)
1419
+ }
1420
+
1421
+ const deleteEndpoint = (index) => {
1422
+ const ep = endpoints[index]
1423
+ const newEndpoints = endpoints.filter((_, i) => i !== index)
1424
+ saveEndpoints(newEndpoints)
1425
+ if (selectedEndpoint === ep.alias) setSelectedEndpoint(newEndpoints[0]?.alias || '')
1426
+ if (editingIndex === index) clearForm()
1427
+ }
1428
+
1429
+ const clearForm = () => {
1430
+ setFormAlias('')
1431
+ setFormUrl('')
1432
+ setFormApiKey('')
1433
+ setFormInfo(null)
1434
+ setFormStatus('')
1435
+ setEditingIndex(-1)
1323
1436
  }
1324
1437
 
1325
1438
  const testAI = async () => {
1326
- if (!endpoint) return
1439
+ const ep = endpoints.find(e => e.alias === selectedEndpoint)
1440
+ if (!ep) return
1327
1441
  setTesting(true)
1328
1442
  setTestResult('')
1329
- const proxyUrl = toProxyUrl(endpoint)
1330
- const url = proxyUrl.includes('?') ? proxyUrl + '&mode=sync' : proxyUrl + '?mode=sync'
1331
- addLog('request', 'POST ' + url, { input: testInput })
1332
-
1333
- const startTime = Date.now()
1443
+ const proxyUrl = toProxyUrl(ep.url)
1444
+ addLog('request', 'POST ' + proxyUrl + ' (' + ep.alias + ')', { input: testInput })
1334
1445
  try {
1335
- const res = await fetch(url, {
1446
+ const res = await fetch(proxyUrl + '?mode=sync', {
1336
1447
  method: 'POST',
1337
- headers: {
1338
- 'Content-Type': 'application/json',
1339
- ...(apiKey ? { 'X-API-Key': apiKey } : {})
1340
- },
1448
+ headers: { 'Content-Type': 'application/json', ...(ep.apiKey ? { 'X-API-Key': ep.apiKey } : {}) },
1341
1449
  body: JSON.stringify({ input: { text: testInput }, variables: {} })
1342
1450
  })
1343
1451
  const data = await res.json()
1344
- const duration = Date.now() - startTime
1345
-
1346
1452
  if (!res.ok || data.error) {
1347
- const errMsg = data.detail || data.error?.message || data.error || res.statusText
1348
- setTestResult('Error: ' + errMsg)
1349
- addLog('error', res.status + ' (' + duration + 'ms)', data)
1453
+ setTestResult('Error: ' + (data.detail || data.error?.message || data.error || res.statusText))
1454
+ addLog('error', res.status + '', data)
1350
1455
  } else {
1351
- // Sync mode returns data.data.output or data.data.response
1352
1456
  const output = data.data?.output || data.data?.response || data.output || data.response || JSON.stringify(data, null, 2)
1353
1457
  const outputStr = typeof output === 'string' ? output : JSON.stringify(output, null, 2)
1354
1458
  setTestResult(outputStr)
1355
- addLog('success', '200 OK (' + duration + 'ms)', { output: outputStr.substring(0, 200) })
1459
+ addLog('success', '200 OK', { output: outputStr.substring(0, 200) })
1356
1460
  }
1357
1461
  } catch (err) {
1358
1462
  setTestResult('Error: ' + err.message)
@@ -1361,17 +1465,39 @@ export default function DevAdminPage() {
1361
1465
  setTesting(false)
1362
1466
  }
1363
1467
 
1468
+ const downloadManifest = () => {
1469
+ const manifest = localStorage.getItem('promptline_manifest')
1470
+ if (!manifest) return
1471
+ const blob = new Blob([manifest], { type: 'application/json' })
1472
+ const url = URL.createObjectURL(blob)
1473
+ const a = document.createElement('a')
1474
+ a.href = url
1475
+ a.download = 'promptline.config.json'
1476
+ a.click()
1477
+ URL.revokeObjectURL(url)
1478
+ addLog('info', 'Downloaded promptline.config.json')
1479
+ }
1480
+
1364
1481
  return (
1365
1482
  <div className="min-h-screen bg-gray-900 text-white p-8">
1366
- <div className="max-w-2xl mx-auto">
1483
+ <div className="max-w-4xl mx-auto">
1367
1484
  <div className="flex items-center justify-between mb-8">
1368
1485
  <div>
1369
1486
  <h1 className="text-3xl font-bold text-yellow-400">/_dev</h1>
1370
- <p className="text-gray-400 mt-1">PromptLine Endpoint (Local Only)</p>
1487
+ <p className="text-gray-400 mt-1">PromptLine Endpoints (Local Only)</p>
1488
+ </div>
1489
+ <div className="flex gap-3">
1490
+ <button
1491
+ onClick={downloadManifest}
1492
+ disabled={endpoints.length === 0}
1493
+ className="px-4 py-2 bg-indigo-600 rounded hover:bg-indigo-500 disabled:opacity-50"
1494
+ >
1495
+ Download Manifest
1496
+ </button>
1497
+ <Link to="/" className="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600">
1498
+ Back to App
1499
+ </Link>
1371
1500
  </div>
1372
- <Link to="/" className="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600">
1373
- Back to App
1374
- </Link>
1375
1501
  </div>
1376
1502
 
1377
1503
  <div className="bg-yellow-900/30 border border-yellow-600 rounded-lg p-4 mb-8">
@@ -1380,72 +1506,186 @@ export default function DevAdminPage() {
1380
1506
  </p>
1381
1507
  </div>
1382
1508
 
1383
- {/* Configuration */}
1509
+ {/* Your Pages */}
1384
1510
  <div className="bg-gray-800 rounded-lg p-6 mb-8">
1385
- <div className="flex items-center justify-between mb-4">
1386
- <h2 className="text-xl font-semibold">Configuration</h2>
1387
- {saved && (
1388
- <span className="px-2 py-1 bg-green-600/30 text-green-400 text-xs rounded">Saved</span>
1389
- )}
1511
+ <h2 className="text-xl font-semibold mb-4">Your Pages</h2>
1512
+ <div className="grid grid-cols-2 gap-4">
1513
+ <div>
1514
+ <h3 className="text-sm font-medium text-gray-400 mb-2">Public</h3>
1515
+ <div className="space-y-2">
1516
+ <Link to="/" className="block px-3 py-2 bg-gray-700 rounded hover:bg-gray-600 text-blue-400">
1517
+ / → index.tsx
1518
+ </Link>
1519
+ </div>
1520
+ </div>
1521
+ <div>
1522
+ <h3 className="text-sm font-medium text-gray-400 mb-2">Private (auth required)</h3>
1523
+ <div className="space-y-2">
1524
+ <Link to="/dashboard" className="block px-3 py-2 bg-gray-700 rounded hover:bg-gray-600 text-blue-400">
1525
+ /dashboard → dashboard.tsx
1526
+ </Link>
1527
+ <Link to="/settings" className="block px-3 py-2 bg-gray-700 rounded hover:bg-gray-600 text-blue-400">
1528
+ /settings → settings.tsx
1529
+ </Link>
1530
+ </div>
1531
+ </div>
1390
1532
  </div>
1533
+ </div>
1534
+
1535
+ {/* Saved Endpoints List */}
1536
+ {endpoints.length > 0 && (
1537
+ <div className="bg-gray-800 rounded-lg p-6 mb-8">
1538
+ <h2 className="text-xl font-semibold mb-4">Configured Endpoints ({endpoints.length})</h2>
1539
+ <div className="space-y-3">
1540
+ {endpoints.map((ep, i) => (
1541
+ <div key={ep.alias} className="flex items-center justify-between bg-gray-700 rounded p-4">
1542
+ <div className="flex items-center gap-4">
1543
+ <span className={
1544
+ ep.status === 'valid' ? 'text-green-400' :
1545
+ ep.status === 'error' ? 'text-red-400' :
1546
+ 'text-gray-400'
1547
+ }>●</span>
1548
+ <div>
1549
+ <div className="font-medium text-white">{ep.alias}</div>
1550
+ <div className="text-sm text-gray-400">
1551
+ {ep.info?.name || 'No info'} • {ep.info?.endpoint_type || 'unknown'}
1552
+ </div>
1553
+ </div>
1554
+ </div>
1555
+ <div className="flex gap-2">
1556
+ <button
1557
+ onClick={() => editEndpoint(i)}
1558
+ className="px-3 py-1 bg-gray-600 rounded text-sm hover:bg-gray-500"
1559
+ >
1560
+ Edit
1561
+ </button>
1562
+ <button
1563
+ onClick={() => deleteEndpoint(i)}
1564
+ className="px-3 py-1 bg-red-600/30 text-red-400 rounded text-sm hover:bg-red-600/50"
1565
+ >
1566
+ Delete
1567
+ </button>
1568
+ </div>
1569
+ </div>
1570
+ ))}
1571
+ </div>
1572
+ </div>
1573
+ )}
1574
+
1575
+ {/* Add/Edit Endpoint Form */}
1576
+ <div className="bg-gray-800 rounded-lg p-6 mb-8">
1577
+ <h2 className="text-xl font-semibold mb-4">
1578
+ {editingIndex >= 0 ? $BACKTICK}Edit Endpoint: \${formAlias}$BACKTICK : 'Add New Endpoint'}
1579
+ </h2>
1391
1580
 
1392
1581
  <div className="space-y-4">
1582
+ <div>
1583
+ <label className="block text-sm text-gray-400 mb-1">Alias (used in code)</label>
1584
+ <input
1585
+ value={formAlias}
1586
+ onChange={(e) => setFormAlias(e.target.value.replace(/[^a-zA-Z0-9_-]/g, ''))}
1587
+ className="w-full bg-gray-700 p-3 rounded text-white font-mono text-sm"
1588
+ placeholder="chatbot, traduction, analyse..."
1589
+ />
1590
+ <p className="text-xs text-gray-500 mt-1">Used in usePromptLine('{formAlias || 'alias'}')</p>
1591
+ </div>
1592
+
1393
1593
  <div>
1394
1594
  <label className="block text-sm text-gray-400 mb-1">Endpoint URL</label>
1395
1595
  <input
1396
- value={endpoint}
1397
- onChange={(e) => { setEndpoint(e.target.value); setSaved(false) }}
1596
+ value={formUrl}
1597
+ onChange={(e) => setFormUrl(e.target.value)}
1398
1598
  className="w-full bg-gray-700 p-3 rounded text-white font-mono text-sm"
1399
1599
  placeholder="https://app.promptlineops.com/api/v1/live/..."
1400
1600
  />
1401
- <p className="text-xs text-gray-500 mt-1">Copy the Live API URL from Creator Studio</p>
1402
1601
  </div>
1403
1602
 
1404
1603
  <div>
1405
1604
  <label className="block text-sm text-gray-400 mb-1">API Key</label>
1406
1605
  <input
1407
1606
  type="password"
1408
- value={apiKey}
1409
- onChange={(e) => { setApiKey(e.target.value); setSaved(false) }}
1607
+ value={formApiKey}
1608
+ onChange={(e) => setFormApiKey(e.target.value)}
1410
1609
  className="w-full bg-gray-700 p-3 rounded text-white font-mono text-sm"
1411
- placeholder="Your API key..."
1610
+ placeholder="sk_org_... or sk_ws_..."
1412
1611
  />
1413
- <p className="text-xs text-gray-500 mt-1">Get your API key from /api-keys</p>
1414
1612
  </div>
1415
1613
 
1416
- <div className="flex gap-3">
1614
+ <div className="flex gap-3 flex-wrap">
1417
1615
  <button
1418
- onClick={saveConfig}
1419
- disabled={!endpoint}
1420
- className="px-6 py-2 bg-indigo-600 rounded font-medium hover:bg-indigo-500 disabled:opacity-50"
1616
+ onClick={checkHealth}
1617
+ disabled={!formUrl || !formApiKey || formStatus === 'checking'}
1618
+ className="px-4 py-2 bg-gray-600 rounded hover:bg-gray-500 disabled:opacity-50 flex items-center gap-2"
1619
+ >
1620
+ {formStatus === 'checking' ? 'Checking...' : 'Check Health'}
1621
+ {formStatus === 'valid' && <span className="text-green-400">●</span>}
1622
+ {formStatus === 'error' && <span className="text-red-400">●</span>}
1623
+ </button>
1624
+ <button
1625
+ onClick={fetchInfo}
1626
+ disabled={!formUrl || !formApiKey}
1627
+ className="px-4 py-2 bg-gray-600 rounded hover:bg-gray-500 disabled:opacity-50"
1421
1628
  >
1422
- Save
1629
+ Fetch Info
1423
1630
  </button>
1424
1631
  <button
1425
- onClick={testConnection}
1426
- disabled={!endpoint || status === 'testing'}
1427
- className="px-6 py-2 bg-gray-600 rounded font-medium hover:bg-gray-500 disabled:opacity-50 flex items-center gap-2"
1632
+ onClick={saveEndpoint}
1633
+ disabled={!formAlias || !formUrl}
1634
+ className="px-6 py-2 bg-indigo-600 rounded font-medium hover:bg-indigo-500 disabled:opacity-50"
1428
1635
  >
1429
- {status === 'testing' ? 'Testing...' : 'Test Connection'}
1430
- {status === 'connected' && <span className="text-green-400">●</span>}
1431
- {status === 'error' && <span className="text-red-400">●</span>}
1636
+ {editingIndex >= 0 ? 'Update' : 'Add Endpoint'}
1432
1637
  </button>
1433
- {saved && (
1638
+ {(formAlias || formUrl || editingIndex >= 0) && (
1434
1639
  <button
1435
- onClick={clearConfig}
1436
- className="px-4 py-2 bg-red-600/30 text-red-400 rounded hover:bg-red-600/50"
1640
+ onClick={clearForm}
1641
+ className="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600"
1437
1642
  >
1438
- Clear
1643
+ Cancel
1439
1644
  </button>
1440
1645
  )}
1441
1646
  </div>
1442
1647
  </div>
1648
+
1649
+ {/* Info Preview */}
1650
+ {formInfo && (
1651
+ <div className="mt-6 p-4 bg-gray-700 rounded">
1652
+ <h3 className="font-medium mb-3 text-gray-300">Endpoint Info</h3>
1653
+ <div className="grid grid-cols-2 gap-3 text-sm">
1654
+ <div><span className="text-gray-400">Name:</span> <span className="text-white">{formInfo.name}</span></div>
1655
+ <div><span className="text-gray-400">Status:</span> <span className={formInfo.status === 'active' ? 'text-green-400' : 'text-red-400'}>{formInfo.status}</span></div>
1656
+ <div><span className="text-gray-400">Type:</span> <span className="text-white">{formInfo.endpoint_type}</span></div>
1657
+ <div><span className="text-gray-400">Model:</span> <span className="text-white">{formInfo.ai_model}</span></div>
1658
+ {formInfo.required_variables?.length > 0 && (
1659
+ <div className="col-span-2">
1660
+ <span className="text-gray-400">Variables:</span>
1661
+ <span className="ml-2">{formInfo.required_variables.map(v => (
1662
+ <span key={v} className="ml-1 px-2 py-0.5 bg-gray-600 rounded text-xs text-blue-300">{v}</span>
1663
+ ))}</span>
1664
+ </div>
1665
+ )}
1666
+ </div>
1667
+ </div>
1668
+ )}
1443
1669
  </div>
1444
1670
 
1445
1671
  {/* Test AI */}
1446
- {saved && (
1672
+ {endpoints.length > 0 && (
1447
1673
  <div className="bg-gray-800 rounded-lg p-6 mb-8">
1448
1674
  <h2 className="text-xl font-semibold mb-4">Test AI</h2>
1675
+
1676
+ <div className="mb-4">
1677
+ <label className="block text-sm text-gray-400 mb-1">Select Endpoint</label>
1678
+ <select
1679
+ value={selectedEndpoint}
1680
+ onChange={(e) => setSelectedEndpoint(e.target.value)}
1681
+ className="w-full bg-gray-700 p-3 rounded text-white"
1682
+ >
1683
+ {endpoints.map(ep => (
1684
+ <option key={ep.alias} value={ep.alias}>{ep.alias} - {ep.info?.name || 'Unknown'}</option>
1685
+ ))}
1686
+ </select>
1687
+ </div>
1688
+
1449
1689
  <textarea
1450
1690
  value={testInput}
1451
1691
  onChange={(e) => setTestInput(e.target.value)}
@@ -1455,7 +1695,7 @@ export default function DevAdminPage() {
1455
1695
  />
1456
1696
  <button
1457
1697
  onClick={testAI}
1458
- disabled={testing}
1698
+ disabled={testing || !selectedEndpoint}
1459
1699
  className="px-6 py-2 bg-green-600 rounded font-medium hover:bg-green-500 disabled:opacity-50"
1460
1700
  >
1461
1701
  {testing ? 'Sending...' : 'Send Request'}
@@ -1470,7 +1710,7 @@ export default function DevAdminPage() {
1470
1710
  )}
1471
1711
 
1472
1712
  {/* Logs */}
1473
- <div className="bg-gray-800 rounded-lg p-6">
1713
+ <div className="bg-gray-800 rounded-lg p-6 mb-8">
1474
1714
  <div className="flex items-center justify-between mb-4">
1475
1715
  <h2 className="text-xl font-semibold">Logs</h2>
1476
1716
  <button onClick={() => setLogs([])} className="px-3 py-1 bg-gray-700 rounded text-sm hover:bg-gray-600">
@@ -1505,13 +1745,18 @@ export default function DevAdminPage() {
1505
1745
  </div>
1506
1746
 
1507
1747
  {/* Usage */}
1508
- <div className="mt-8 p-6 bg-gray-800 rounded-lg">
1748
+ <div className="bg-gray-800 rounded-lg p-6">
1509
1749
  <h2 className="text-xl font-semibold mb-4">Usage in Your Code</h2>
1510
1750
  <pre className="bg-gray-900 p-4 rounded text-sm overflow-x-auto">
1511
- {$BACKTICK}const { callAI } = usePromptLine()
1751
+ {$BACKTICK}// Use specific endpoint by alias
1752
+ const { execute } = usePromptLine('\${endpoints[0]?.alias || 'chatbot'}')
1753
+
1754
+ const result = await execute({ text: 'Hello!' })
1755
+ console.log(result.response)
1512
1756
 
1513
- const result = await callAI('main', { text: 'Hello!' })
1514
- console.log(result.response)$BACKTICK}
1757
+ // Multiple endpoints
1758
+ const { execute: chat } = usePromptLine('chatbot')
1759
+ const { execute: translate } = usePromptLine('traduction')$BACKTICK}
1515
1760
  </pre>
1516
1761
  </div>
1517
1762
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptlineapp",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Create PromptLine applications with ease",
5
5
  "author": "PromptLine <support@promptlineops.com>",
6
6
  "license": "MIT",