promptlineapp 1.4.1 → 1.6.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.
- package/README.md +45 -107
- package/bin/cli.js +574 -193
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,136 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-promptline-app
|
|
2
2
|
|
|
3
|
-
Create AI-powered
|
|
3
|
+
Create AI-powered apps for the [PromptLine Marketplace](https://promptlineops.com).
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npx promptlineapp my-app
|
|
9
9
|
cd my-app
|
|
10
|
+
npm run dev
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
## Local Development
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
### 1. Configure your endpoints
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
Open **http://localhost:5173/_dev** and add your PromptLine endpoints:
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
```
|
|
35
|
-
|
|
36
|
-
## Template Presets
|
|
37
|
-
|
|
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 |
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
npx promptlineapp my-app --preset full-app
|
|
45
|
-
npx promptlineapp my-api --preset api
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## What You Get
|
|
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_...`)
|
|
49
22
|
|
|
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
|
-
```
|
|
23
|
+
Click "Check Health" to verify, then "Add Endpoint".
|
|
64
24
|
|
|
65
|
-
|
|
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
|
-
```
|
|
25
|
+
### 2. Use in your code
|
|
93
26
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
In your React pages:
|
|
97
|
-
|
|
98
|
-
```tsx
|
|
27
|
+
```jsx
|
|
99
28
|
import { usePromptLine } from '@promptline/sdk'
|
|
100
29
|
|
|
101
|
-
|
|
102
|
-
const {
|
|
103
|
-
|
|
104
|
-
// Access instance config
|
|
105
|
-
const appName = config.app_name
|
|
30
|
+
function MyComponent() {
|
|
31
|
+
const { execute, isConfigured } = usePromptLine('chatbot')
|
|
106
32
|
|
|
107
|
-
|
|
108
|
-
|
|
33
|
+
const handleClick = async () => {
|
|
34
|
+
const result = await execute({ text: 'Hello!' })
|
|
35
|
+
console.log(result.response)
|
|
36
|
+
}
|
|
109
37
|
|
|
110
|
-
|
|
111
|
-
|
|
38
|
+
if (!isConfigured) {
|
|
39
|
+
return <p>Configure 'chatbot' at <a href="/_dev">/_dev</a></p>
|
|
40
|
+
}
|
|
112
41
|
|
|
113
|
-
return <
|
|
42
|
+
return <button onClick={handleClick}>Ask AI</button>
|
|
114
43
|
}
|
|
115
44
|
```
|
|
116
45
|
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
46
|
+
### 3. Multiple endpoints
|
|
122
47
|
|
|
123
|
-
|
|
48
|
+
```jsx
|
|
49
|
+
const { execute: chat } = usePromptLine('chatbot')
|
|
50
|
+
const { execute: translate } = usePromptLine('traduction')
|
|
124
51
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
52
|
+
// Use them independently
|
|
53
|
+
const chatResult = await chat({ text: 'Hello' })
|
|
54
|
+
const translatedResult = await translate({ text: 'Bonjour', target: 'en' })
|
|
55
|
+
```
|
|
128
56
|
|
|
129
|
-
##
|
|
57
|
+
## Project Structure
|
|
130
58
|
|
|
131
|
-
|
|
132
|
-
-
|
|
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
|
-
##
|
|
70
|
+
## Deployment
|
|
135
71
|
|
|
136
|
-
|
|
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:
|
|
1082
|
+
open: '/_dev',
|
|
1080
1083
|
strictPort: false,
|
|
1081
1084
|
proxy: {
|
|
1082
|
-
//
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
|
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
|
|
1123
|
-
function
|
|
1134
|
+
// Get all configured endpoints from localStorage
|
|
1135
|
+
function getEndpoints() {
|
|
1124
1136
|
try {
|
|
1125
|
-
return JSON.parse(localStorage.getItem(
|
|
1126
|
-
} catch { return
|
|
1137
|
+
return JSON.parse(localStorage.getItem(ENDPOINTS_STORAGE_KEY) || '[]')
|
|
1138
|
+
} catch { return [] }
|
|
1127
1139
|
}
|
|
1128
1140
|
|
|
1129
|
-
//
|
|
1130
|
-
|
|
1131
|
-
const
|
|
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
|
-
|
|
1134
|
-
|
|
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
|
+
}
|
|
1135
1159
|
|
|
1136
|
-
|
|
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'
|
|
1165
|
+
|
|
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)
|
|
1149
|
-
|
|
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 {
|
|
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
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
-
|
|
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
|
|
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 [
|
|
1234
|
-
const [
|
|
1235
|
-
const [
|
|
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,121 +1318,145 @@ export default function DevAdminPage() {
|
|
|
1245
1318
|
}
|
|
1246
1319
|
|
|
1247
1320
|
useEffect(() => {
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
-
|
|
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
|
|
1289
|
-
if (!
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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'))
|
|
1359
|
+
try {
|
|
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 })
|
|
1294
1372
|
}
|
|
1295
|
-
|
|
1296
|
-
const proxyUrl = toProxyUrl(endpoint)
|
|
1297
|
-
// Use /isAlive endpoint for health check (no LLM execution, no token consumption)
|
|
1298
|
-
const isAliveUrl = proxyUrl + '/isAlive'
|
|
1299
|
-
addLog('info', 'Testing connection (isAlive)...', { endpoint: isAliveUrl, original: endpoint })
|
|
1373
|
+
}
|
|
1300
1374
|
|
|
1301
|
-
|
|
1375
|
+
const fetchInfo = async () => {
|
|
1376
|
+
if (!formUrl || !formApiKey) return
|
|
1377
|
+
const proxyUrl = toProxyUrl(formUrl)
|
|
1378
|
+
addLog('info', 'Fetching info: ' + (formAlias || 'new endpoint'))
|
|
1302
1379
|
try {
|
|
1303
|
-
const res = await fetch(
|
|
1304
|
-
method: 'GET',
|
|
1305
|
-
headers: {
|
|
1306
|
-
'X-API-Key': apiKey
|
|
1307
|
-
}
|
|
1308
|
-
})
|
|
1380
|
+
const res = await fetch(proxyUrl + '/info', { method: 'GET', headers: { 'X-API-Key': formApiKey } })
|
|
1309
1381
|
const data = await res.json().catch(() => ({}))
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
setStatus('connected')
|
|
1314
|
-
addLog('success', 'Endpoint is alive (' + duration + 'ms)', { alive: true })
|
|
1315
|
-
} else if (res.ok && data.alive === false) {
|
|
1316
|
-
setStatus('error')
|
|
1317
|
-
addLog('error', 'Endpoint exists but is inactive (' + duration + 'ms)', { alive: false })
|
|
1382
|
+
if (res.ok) {
|
|
1383
|
+
setFormInfo(data)
|
|
1384
|
+
addLog('success', 'Info retrieved', data)
|
|
1318
1385
|
} else {
|
|
1319
|
-
|
|
1320
|
-
addLog('error', res.status + ' ' + res.statusText + ' (' + duration + 'ms)', data)
|
|
1386
|
+
addLog('error', 'Info fetch failed: ' + res.status, data)
|
|
1321
1387
|
}
|
|
1322
1388
|
} catch (err) {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
|
1329
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
|
|
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)
|
|
1330
1436
|
}
|
|
1331
1437
|
|
|
1332
1438
|
const testAI = async () => {
|
|
1333
|
-
|
|
1439
|
+
const ep = endpoints.find(e => e.alias === selectedEndpoint)
|
|
1440
|
+
if (!ep) return
|
|
1334
1441
|
setTesting(true)
|
|
1335
1442
|
setTestResult('')
|
|
1336
|
-
const proxyUrl = toProxyUrl(
|
|
1337
|
-
|
|
1338
|
-
addLog('request', 'POST ' + url, { input: testInput })
|
|
1339
|
-
|
|
1340
|
-
const startTime = Date.now()
|
|
1443
|
+
const proxyUrl = toProxyUrl(ep.url)
|
|
1444
|
+
addLog('request', 'POST ' + proxyUrl + ' (' + ep.alias + ')', { input: testInput })
|
|
1341
1445
|
try {
|
|
1342
|
-
const res = await fetch(
|
|
1446
|
+
const res = await fetch(proxyUrl + '?mode=sync', {
|
|
1343
1447
|
method: 'POST',
|
|
1344
|
-
headers: {
|
|
1345
|
-
'Content-Type': 'application/json',
|
|
1346
|
-
...(apiKey ? { 'X-API-Key': apiKey } : {})
|
|
1347
|
-
},
|
|
1448
|
+
headers: { 'Content-Type': 'application/json', ...(ep.apiKey ? { 'X-API-Key': ep.apiKey } : {}) },
|
|
1348
1449
|
body: JSON.stringify({ input: { text: testInput }, variables: {} })
|
|
1349
1450
|
})
|
|
1350
1451
|
const data = await res.json()
|
|
1351
|
-
const duration = Date.now() - startTime
|
|
1352
|
-
|
|
1353
1452
|
if (!res.ok || data.error) {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
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)
|
|
1357
1455
|
} else {
|
|
1358
|
-
// Sync mode returns data.data.output or data.data.response
|
|
1359
1456
|
const output = data.data?.output || data.data?.response || data.output || data.response || JSON.stringify(data, null, 2)
|
|
1360
1457
|
const outputStr = typeof output === 'string' ? output : JSON.stringify(output, null, 2)
|
|
1361
1458
|
setTestResult(outputStr)
|
|
1362
|
-
addLog('success', '200 OK
|
|
1459
|
+
addLog('success', '200 OK', { output: outputStr.substring(0, 200) })
|
|
1363
1460
|
}
|
|
1364
1461
|
} catch (err) {
|
|
1365
1462
|
setTestResult('Error: ' + err.message)
|
|
@@ -1368,17 +1465,39 @@ export default function DevAdminPage() {
|
|
|
1368
1465
|
setTesting(false)
|
|
1369
1466
|
}
|
|
1370
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
|
+
|
|
1371
1481
|
return (
|
|
1372
1482
|
<div className="min-h-screen bg-gray-900 text-white p-8">
|
|
1373
|
-
<div className="max-w-
|
|
1483
|
+
<div className="max-w-4xl mx-auto">
|
|
1374
1484
|
<div className="flex items-center justify-between mb-8">
|
|
1375
1485
|
<div>
|
|
1376
1486
|
<h1 className="text-3xl font-bold text-yellow-400">/_dev</h1>
|
|
1377
|
-
<p className="text-gray-400 mt-1">PromptLine
|
|
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>
|
|
1378
1500
|
</div>
|
|
1379
|
-
<Link to="/" className="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600">
|
|
1380
|
-
Back to App
|
|
1381
|
-
</Link>
|
|
1382
1501
|
</div>
|
|
1383
1502
|
|
|
1384
1503
|
<div className="bg-yellow-900/30 border border-yellow-600 rounded-lg p-4 mb-8">
|
|
@@ -1387,73 +1506,186 @@ export default function DevAdminPage() {
|
|
|
1387
1506
|
</p>
|
|
1388
1507
|
</div>
|
|
1389
1508
|
|
|
1390
|
-
{/*
|
|
1509
|
+
{/* Your Pages */}
|
|
1391
1510
|
<div className="bg-gray-800 rounded-lg p-6 mb-8">
|
|
1392
|
-
<
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
<
|
|
1396
|
-
|
|
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>
|
|
1397
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>
|
|
1398
1580
|
|
|
1399
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
|
+
|
|
1400
1593
|
<div>
|
|
1401
1594
|
<label className="block text-sm text-gray-400 mb-1">Endpoint URL</label>
|
|
1402
1595
|
<input
|
|
1403
|
-
value={
|
|
1404
|
-
onChange={(e) =>
|
|
1596
|
+
value={formUrl}
|
|
1597
|
+
onChange={(e) => setFormUrl(e.target.value)}
|
|
1405
1598
|
className="w-full bg-gray-700 p-3 rounded text-white font-mono text-sm"
|
|
1406
1599
|
placeholder="https://app.promptlineops.com/api/v1/live/..."
|
|
1407
1600
|
/>
|
|
1408
|
-
<p className="text-xs text-gray-500 mt-1">Copy the Live API URL from Creator Studio</p>
|
|
1409
1601
|
</div>
|
|
1410
1602
|
|
|
1411
1603
|
<div>
|
|
1412
1604
|
<label className="block text-sm text-gray-400 mb-1">API Key</label>
|
|
1413
1605
|
<input
|
|
1414
1606
|
type="password"
|
|
1415
|
-
value={
|
|
1416
|
-
onChange={(e) =>
|
|
1607
|
+
value={formApiKey}
|
|
1608
|
+
onChange={(e) => setFormApiKey(e.target.value)}
|
|
1417
1609
|
className="w-full bg-gray-700 p-3 rounded text-white font-mono text-sm"
|
|
1418
|
-
placeholder="
|
|
1610
|
+
placeholder="sk_org_... or sk_ws_..."
|
|
1419
1611
|
/>
|
|
1420
|
-
<p className="text-xs text-gray-500 mt-1">Required for health check. Get from /api-keys</p>
|
|
1421
1612
|
</div>
|
|
1422
1613
|
|
|
1423
|
-
<div className="flex gap-3">
|
|
1614
|
+
<div className="flex gap-3 flex-wrap">
|
|
1424
1615
|
<button
|
|
1425
|
-
onClick={
|
|
1426
|
-
disabled={!
|
|
1427
|
-
className="px-
|
|
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"
|
|
1428
1628
|
>
|
|
1429
|
-
|
|
1629
|
+
Fetch Info
|
|
1430
1630
|
</button>
|
|
1431
1631
|
<button
|
|
1432
|
-
onClick={
|
|
1433
|
-
disabled={!
|
|
1434
|
-
className="px-6 py-2 bg-
|
|
1435
|
-
title="Uses /isAlive endpoint (no LLM cost)"
|
|
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"
|
|
1436
1635
|
>
|
|
1437
|
-
{
|
|
1438
|
-
{status === 'connected' && <span className="text-green-400">●</span>}
|
|
1439
|
-
{status === 'error' && <span className="text-red-400">●</span>}
|
|
1636
|
+
{editingIndex >= 0 ? 'Update' : 'Add Endpoint'}
|
|
1440
1637
|
</button>
|
|
1441
|
-
{
|
|
1638
|
+
{(formAlias || formUrl || editingIndex >= 0) && (
|
|
1442
1639
|
<button
|
|
1443
|
-
onClick={
|
|
1444
|
-
className="px-4 py-2 bg-
|
|
1640
|
+
onClick={clearForm}
|
|
1641
|
+
className="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600"
|
|
1445
1642
|
>
|
|
1446
|
-
|
|
1643
|
+
Cancel
|
|
1447
1644
|
</button>
|
|
1448
1645
|
)}
|
|
1449
1646
|
</div>
|
|
1450
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
|
+
)}
|
|
1451
1669
|
</div>
|
|
1452
1670
|
|
|
1453
1671
|
{/* Test AI */}
|
|
1454
|
-
{
|
|
1672
|
+
{endpoints.length > 0 && (
|
|
1455
1673
|
<div className="bg-gray-800 rounded-lg p-6 mb-8">
|
|
1456
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
|
+
|
|
1457
1689
|
<textarea
|
|
1458
1690
|
value={testInput}
|
|
1459
1691
|
onChange={(e) => setTestInput(e.target.value)}
|
|
@@ -1463,7 +1695,7 @@ export default function DevAdminPage() {
|
|
|
1463
1695
|
/>
|
|
1464
1696
|
<button
|
|
1465
1697
|
onClick={testAI}
|
|
1466
|
-
disabled={testing}
|
|
1698
|
+
disabled={testing || !selectedEndpoint}
|
|
1467
1699
|
className="px-6 py-2 bg-green-600 rounded font-medium hover:bg-green-500 disabled:opacity-50"
|
|
1468
1700
|
>
|
|
1469
1701
|
{testing ? 'Sending...' : 'Send Request'}
|
|
@@ -1478,7 +1710,7 @@ export default function DevAdminPage() {
|
|
|
1478
1710
|
)}
|
|
1479
1711
|
|
|
1480
1712
|
{/* Logs */}
|
|
1481
|
-
<div className="bg-gray-800 rounded-lg p-6">
|
|
1713
|
+
<div className="bg-gray-800 rounded-lg p-6 mb-8">
|
|
1482
1714
|
<div className="flex items-center justify-between mb-4">
|
|
1483
1715
|
<h2 className="text-xl font-semibold">Logs</h2>
|
|
1484
1716
|
<button onClick={() => setLogs([])} className="px-3 py-1 bg-gray-700 rounded text-sm hover:bg-gray-600">
|
|
@@ -1513,13 +1745,18 @@ export default function DevAdminPage() {
|
|
|
1513
1745
|
</div>
|
|
1514
1746
|
|
|
1515
1747
|
{/* Usage */}
|
|
1516
|
-
<div className="
|
|
1748
|
+
<div className="bg-gray-800 rounded-lg p-6">
|
|
1517
1749
|
<h2 className="text-xl font-semibold mb-4">Usage in Your Code</h2>
|
|
1518
1750
|
<pre className="bg-gray-900 p-4 rounded text-sm overflow-x-auto">
|
|
1519
|
-
{$BACKTICK}
|
|
1751
|
+
{$BACKTICK}// Use specific endpoint by alias
|
|
1752
|
+
const { execute } = usePromptLine('\${endpoints[0]?.alias || 'chatbot'}')
|
|
1520
1753
|
|
|
1521
|
-
const result = await
|
|
1522
|
-
console.log(result.response)
|
|
1754
|
+
const result = await execute({ text: 'Hello!' })
|
|
1755
|
+
console.log(result.response)
|
|
1756
|
+
|
|
1757
|
+
// Multiple endpoints
|
|
1758
|
+
const { execute: chat } = usePromptLine('chatbot')
|
|
1759
|
+
const { execute: translate } = usePromptLine('traduction')$BACKTICK}
|
|
1523
1760
|
</pre>
|
|
1524
1761
|
</div>
|
|
1525
1762
|
</div>
|
|
@@ -1822,13 +2059,26 @@ ${c.bold}Documentation:${c.reset} ${c.cyan}https://docs.promptlineops.com${c.res
|
|
|
1822
2059
|
function parseArgs() {
|
|
1823
2060
|
const args = process.argv.slice(2);
|
|
1824
2061
|
const options = {
|
|
2062
|
+
command: 'new', // 'new' or 'get'
|
|
1825
2063
|
name: null,
|
|
2064
|
+
source: null, // Git URL for 'get' command
|
|
1826
2065
|
preset: null,
|
|
1827
2066
|
yes: false,
|
|
1828
2067
|
help: false,
|
|
1829
2068
|
version: false,
|
|
1830
2069
|
};
|
|
1831
2070
|
|
|
2071
|
+
// Detect 'get' command
|
|
2072
|
+
if (args[0] === 'get') {
|
|
2073
|
+
options.command = 'get';
|
|
2074
|
+
options.source = args[1];
|
|
2075
|
+
// Optional: destination name as third argument
|
|
2076
|
+
if (args[2] && !args[2].startsWith('-')) {
|
|
2077
|
+
options.name = args[2];
|
|
2078
|
+
}
|
|
2079
|
+
return options;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1832
2082
|
for (let i = 0; i < args.length; i++) {
|
|
1833
2083
|
const arg = args[i];
|
|
1834
2084
|
|
|
@@ -1848,23 +2098,138 @@ function parseArgs() {
|
|
|
1848
2098
|
return options;
|
|
1849
2099
|
}
|
|
1850
2100
|
|
|
2101
|
+
// Get project from Git repository
|
|
2102
|
+
async function getProject(gitUrl, destName) {
|
|
2103
|
+
const { execSync } = require('child_process');
|
|
2104
|
+
|
|
2105
|
+
printLogo();
|
|
2106
|
+
|
|
2107
|
+
// Extract repo name from URL if destName not provided
|
|
2108
|
+
if (!destName) {
|
|
2109
|
+
// Handle various URL formats
|
|
2110
|
+
// https://github.com/user/repo.git -> repo
|
|
2111
|
+
// git@github.com:user/repo.git -> repo
|
|
2112
|
+
const match = gitUrl.match(/\/([^\/]+?)(\.git)?$/) || gitUrl.match(/:([^\/]+?)(\.git)?$/);
|
|
2113
|
+
destName = match ? match[1] : 'promptline-app';
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// Validate destination name
|
|
2117
|
+
const validated = validatePackageName(destName);
|
|
2118
|
+
if (!validated.valid) {
|
|
2119
|
+
error(`Invalid project name "${destName}": ${validated.error}`);
|
|
2120
|
+
process.exit(1);
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
const targetDir = path.resolve(process.cwd(), destName);
|
|
2124
|
+
|
|
2125
|
+
// Check if directory exists
|
|
2126
|
+
if (fs.existsSync(targetDir)) {
|
|
2127
|
+
error(`Directory "${destName}" already exists.`);
|
|
2128
|
+
process.exit(1);
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
console.log(`${c.cyan}→${c.reset} Cloning from ${c.dim}${gitUrl}${c.reset}`);
|
|
2132
|
+
console.log(`${c.cyan}→${c.reset} Destination: ${c.green}${destName}${c.reset}\n`);
|
|
2133
|
+
|
|
2134
|
+
try {
|
|
2135
|
+
// Clone with depth 1 for speed, stdio inherit for git auth prompts
|
|
2136
|
+
execSync(`git clone --depth 1 "${gitUrl}" "${destName}"`, {
|
|
2137
|
+
stdio: 'inherit',
|
|
2138
|
+
cwd: process.cwd()
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
// Remove .git directory
|
|
2142
|
+
const gitDir = path.join(targetDir, '.git');
|
|
2143
|
+
if (fs.existsSync(gitDir)) {
|
|
2144
|
+
fs.rmSync(gitDir, { recursive: true });
|
|
2145
|
+
}
|
|
2146
|
+
success('Removed .git directory');
|
|
2147
|
+
|
|
2148
|
+
// Initialize fresh git repo
|
|
2149
|
+
try {
|
|
2150
|
+
execSync('git init', { cwd: targetDir, stdio: 'pipe' });
|
|
2151
|
+
success('Initialized new git repository');
|
|
2152
|
+
} catch (e) {
|
|
2153
|
+
// Git init is optional
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// Detect project structure for instructions
|
|
2157
|
+
const hasDevRuntime = fs.existsSync(path.join(targetDir, 'dev-runtime'));
|
|
2158
|
+
const hasDev = fs.existsSync(path.join(targetDir, 'dev'));
|
|
2159
|
+
const hasPackageJson = fs.existsSync(path.join(targetDir, 'package.json'));
|
|
2160
|
+
const hasDevRuntimePackage = fs.existsSync(path.join(targetDir, 'dev-runtime', 'package.json'));
|
|
2161
|
+
|
|
2162
|
+
// Success message with appropriate instructions
|
|
2163
|
+
console.log(`
|
|
2164
|
+
${c.green}╔═══════════════════════════════════════════════════════════╗
|
|
2165
|
+
║ ║
|
|
2166
|
+
║ ${c.bold}Success!${c.reset}${c.green} Project ready in ${c.bold}${destName}${c.reset}${c.green} ║
|
|
2167
|
+
║ ║
|
|
2168
|
+
╚═══════════════════════════════════════════════════════════════╝${c.reset}
|
|
2169
|
+
|
|
2170
|
+
${c.bold}Next steps:${c.reset}
|
|
2171
|
+
|
|
2172
|
+
${c.cyan}cd ${destName}${c.reset}`);
|
|
2173
|
+
|
|
2174
|
+
if (hasDevRuntime && hasDevRuntimePackage) {
|
|
2175
|
+
console.log(`
|
|
2176
|
+
${c.dim}# This project has a dev-runtime environment${c.reset}
|
|
2177
|
+
${c.cyan}cd dev-runtime${c.reset}
|
|
2178
|
+
${c.cyan}npm install${c.reset}
|
|
2179
|
+
${c.cyan}npm run dev${c.reset}`);
|
|
2180
|
+
} else if (hasDev || hasPackageJson) {
|
|
2181
|
+
console.log(`
|
|
2182
|
+
${c.cyan}npm install${c.reset}
|
|
2183
|
+
${c.cyan}npm run dev${c.reset}`);
|
|
2184
|
+
} else {
|
|
2185
|
+
console.log(`
|
|
2186
|
+
${c.dim}# Check the project README for setup instructions${c.reset}`);
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
console.log(`
|
|
2190
|
+
${c.bold}Key files:${c.reset}
|
|
2191
|
+
|
|
2192
|
+
${c.yellow}promptline.yaml${c.reset} Main configuration
|
|
2193
|
+
${c.yellow}public/${c.reset} Public pages
|
|
2194
|
+
${c.yellow}private/${c.reset} Admin/dashboard pages
|
|
2195
|
+
|
|
2196
|
+
${c.bold}Documentation:${c.reset} ${c.cyan}https://docs.promptlineops.com${c.reset}
|
|
2197
|
+
`);
|
|
2198
|
+
|
|
2199
|
+
} catch (err) {
|
|
2200
|
+
error(`Failed to clone repository: ${err.message}`);
|
|
2201
|
+
// Cleanup on failure
|
|
2202
|
+
if (fs.existsSync(targetDir)) {
|
|
2203
|
+
fs.rmSync(targetDir, { recursive: true });
|
|
2204
|
+
}
|
|
2205
|
+
process.exit(1);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
1851
2209
|
function showHelp() {
|
|
1852
2210
|
console.log(`
|
|
1853
|
-
${c.bold}
|
|
2211
|
+
${c.bold}promptlineapp${c.reset} - Create and get PromptLine applications
|
|
1854
2212
|
|
|
1855
2213
|
${c.bold}Usage:${c.reset}
|
|
1856
|
-
npx
|
|
2214
|
+
npx promptlineapp <app-name> [options] Create new app from scratch
|
|
2215
|
+
npx promptlineapp get <git-url> [name] Get existing app from Git
|
|
1857
2216
|
|
|
1858
|
-
${c.bold}
|
|
2217
|
+
${c.bold}Create options:${c.reset}
|
|
1859
2218
|
-p, --preset <preset> Template preset (full-app, api)
|
|
1860
2219
|
-y, --yes Skip prompts, use defaults
|
|
2220
|
+
|
|
2221
|
+
${c.bold}General:${c.reset}
|
|
1861
2222
|
-h, --help Show this help
|
|
1862
2223
|
-v, --version Show version
|
|
1863
2224
|
|
|
1864
2225
|
${c.bold}Examples:${c.reset}
|
|
1865
|
-
|
|
1866
|
-
npx
|
|
1867
|
-
npx
|
|
2226
|
+
${c.dim}# Create new app from scratch${c.reset}
|
|
2227
|
+
npx promptlineapp my-app
|
|
2228
|
+
npx promptlineapp my-api --preset api
|
|
2229
|
+
|
|
2230
|
+
${c.dim}# Get existing use case from Git${c.reset}
|
|
2231
|
+
npx promptlineapp get https://github.com/promptline/claude-realestate.git
|
|
2232
|
+
npx promptlineapp get git@github.com:promptline/menumind.git my-restaurant
|
|
1868
2233
|
|
|
1869
2234
|
${c.bold}Presets:${c.reset}
|
|
1870
2235
|
full-app Complete app with pages, dashboard & AI (default)
|
|
@@ -1891,13 +2256,29 @@ async function main() {
|
|
|
1891
2256
|
process.exit(0);
|
|
1892
2257
|
}
|
|
1893
2258
|
|
|
2259
|
+
// Route by command
|
|
2260
|
+
if (options.command === 'get') {
|
|
2261
|
+
if (!options.source) {
|
|
2262
|
+
printLogo();
|
|
2263
|
+
error('Please specify a Git URL:');
|
|
2264
|
+
console.log(`\n ${c.cyan}npx promptlineapp get${c.reset} ${c.green}<git-url>${c.reset} [name]\n`);
|
|
2265
|
+
console.log('For example:');
|
|
2266
|
+
console.log(` ${c.cyan}npx promptlineapp get${c.reset} ${c.green}https://github.com/promptline/claude-realestate.git${c.reset}\n`);
|
|
2267
|
+
console.log(`Run ${c.cyan}npx promptlineapp --help${c.reset} for more options.`);
|
|
2268
|
+
process.exit(1);
|
|
2269
|
+
}
|
|
2270
|
+
await getProject(options.source, options.name);
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// Default: create new app
|
|
1894
2275
|
if (!options.name) {
|
|
1895
2276
|
printLogo();
|
|
1896
2277
|
error('Please specify a project name:');
|
|
1897
|
-
console.log(`\n ${c.cyan}npx
|
|
2278
|
+
console.log(`\n ${c.cyan}npx promptlineapp${c.reset} ${c.green}<app-name>${c.reset}\n`);
|
|
1898
2279
|
console.log('For example:');
|
|
1899
|
-
console.log(` ${c.cyan}npx
|
|
1900
|
-
console.log(`Run ${c.cyan}npx
|
|
2280
|
+
console.log(` ${c.cyan}npx promptlineapp${c.reset} ${c.green}my-restaurant-app${c.reset}\n`);
|
|
2281
|
+
console.log(`Run ${c.cyan}npx promptlineapp --help${c.reset} for more options.`);
|
|
1901
2282
|
process.exit(1);
|
|
1902
2283
|
}
|
|
1903
2284
|
|