snow-ai 0.2.24 → 0.2.25
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/dist/api/chat.d.ts +0 -8
- package/dist/api/chat.js +1 -144
- package/dist/api/responses.d.ts +0 -11
- package/dist/api/responses.js +1 -189
- package/dist/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +80 -295
- package/dist/app.d.ts +2 -1
- package/dist/app.js +11 -13
- package/dist/cli.js +8 -3
- package/dist/hooks/useGlobalNavigation.d.ts +1 -1
- package/dist/mcp/filesystem.d.ts +49 -6
- package/dist/mcp/filesystem.js +243 -86
- package/dist/ui/pages/ChatScreen.d.ts +4 -2
- package/dist/ui/pages/ChatScreen.js +31 -2
- package/dist/ui/pages/{ApiConfigScreen.d.ts → ConfigScreen.d.ts} +1 -1
- package/dist/ui/pages/ConfigScreen.js +549 -0
- package/dist/ui/pages/WelcomeScreen.js +7 -18
- package/dist/utils/apiConfig.d.ts +0 -2
- package/dist/utils/contextCompressor.js +363 -49
- package/dist/utils/retryUtils.js +6 -0
- package/package.json +1 -1
- package/dist/ui/pages/ApiConfigScreen.js +0 -161
- package/dist/ui/pages/ModelConfigScreen.d.ts +0 -8
- package/dist/ui/pages/ModelConfigScreen.js +0 -504
|
@@ -1,5 +1,342 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import {
|
|
2
|
+
import { GoogleGenAI } from '@google/genai';
|
|
3
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
4
|
+
import { getOpenAiConfig, getCustomHeaders, getCustomSystemPrompt } from './apiConfig.js';
|
|
5
|
+
import { SYSTEM_PROMPT } from '../api/systemPrompt.js';
|
|
6
|
+
/**
|
|
7
|
+
* Compression request prompt - asks AI to summarize conversation with focus on task continuity
|
|
8
|
+
*/
|
|
9
|
+
const COMPRESSION_PROMPT = 'Please provide a concise summary of our conversation so far. Focus on: 1) The current task or goal we are working on, 2) Key decisions and approaches we have agreed upon, 3) Important context needed to continue, 4) Any pending or unfinished work. Keep it brief but ensure I can seamlessly continue assisting with the task.';
|
|
10
|
+
/**
|
|
11
|
+
* Compress context using OpenAI Chat Completions API
|
|
12
|
+
*/
|
|
13
|
+
async function compressWithChatCompletions(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
|
|
14
|
+
const client = new OpenAI({
|
|
15
|
+
apiKey,
|
|
16
|
+
baseURL: baseUrl,
|
|
17
|
+
});
|
|
18
|
+
const customHeaders = getCustomHeaders();
|
|
19
|
+
// Build messages with system prompt support
|
|
20
|
+
const messages = [];
|
|
21
|
+
if (systemPrompt) {
|
|
22
|
+
// If custom system prompt exists: custom as system, default as first user message
|
|
23
|
+
messages.push({ role: 'system', content: systemPrompt });
|
|
24
|
+
messages.push({ role: 'user', content: SYSTEM_PROMPT });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// No custom system prompt: default as system
|
|
28
|
+
messages.push({ role: 'system', content: SYSTEM_PROMPT });
|
|
29
|
+
}
|
|
30
|
+
// Add all conversation history (exclude system messages)
|
|
31
|
+
for (const msg of conversationMessages) {
|
|
32
|
+
if (msg.role !== 'system' && msg.role !== 'tool') {
|
|
33
|
+
messages.push({
|
|
34
|
+
role: msg.role,
|
|
35
|
+
content: msg.content,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Add compression request as final user message
|
|
40
|
+
messages.push({
|
|
41
|
+
role: 'user',
|
|
42
|
+
content: COMPRESSION_PROMPT,
|
|
43
|
+
});
|
|
44
|
+
// Build request payload (no tools for compression)
|
|
45
|
+
const requestPayload = {
|
|
46
|
+
model: modelName,
|
|
47
|
+
messages,
|
|
48
|
+
stream: true,
|
|
49
|
+
stream_options: { include_usage: true },
|
|
50
|
+
};
|
|
51
|
+
// Log request payload
|
|
52
|
+
console.log('[ContextCompressor] Chat Completions Request:', JSON.stringify(requestPayload, null, 2));
|
|
53
|
+
// Use streaming to avoid timeout
|
|
54
|
+
const stream = (await client.chat.completions.create(requestPayload, {
|
|
55
|
+
headers: customHeaders,
|
|
56
|
+
}));
|
|
57
|
+
let summary = '';
|
|
58
|
+
let usage = {
|
|
59
|
+
prompt_tokens: 0,
|
|
60
|
+
completion_tokens: 0,
|
|
61
|
+
total_tokens: 0,
|
|
62
|
+
};
|
|
63
|
+
for await (const chunk of stream) {
|
|
64
|
+
const delta = chunk.choices[0]?.delta;
|
|
65
|
+
if (delta?.content) {
|
|
66
|
+
summary += delta.content;
|
|
67
|
+
}
|
|
68
|
+
// Collect usage info (usually in the last chunk)
|
|
69
|
+
if (chunk.usage) {
|
|
70
|
+
usage = {
|
|
71
|
+
prompt_tokens: chunk.usage.prompt_tokens || 0,
|
|
72
|
+
completion_tokens: chunk.usage.completion_tokens || 0,
|
|
73
|
+
total_tokens: chunk.usage.total_tokens || 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!summary) {
|
|
78
|
+
throw new Error('Failed to generate summary from compact model');
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
summary,
|
|
82
|
+
usage,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Compress context using OpenAI Responses API
|
|
87
|
+
*/
|
|
88
|
+
async function compressWithResponses(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
|
|
89
|
+
const client = new OpenAI({
|
|
90
|
+
apiKey,
|
|
91
|
+
baseURL: baseUrl,
|
|
92
|
+
});
|
|
93
|
+
const customHeaders = getCustomHeaders();
|
|
94
|
+
// Build instructions
|
|
95
|
+
const instructions = systemPrompt || SYSTEM_PROMPT;
|
|
96
|
+
// Build input array with conversation history
|
|
97
|
+
const input = [];
|
|
98
|
+
// If custom system prompt exists, add default as first user message
|
|
99
|
+
if (systemPrompt) {
|
|
100
|
+
input.push({
|
|
101
|
+
type: 'message',
|
|
102
|
+
role: 'user',
|
|
103
|
+
content: [{ type: 'input_text', text: SYSTEM_PROMPT }],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// Add all conversation history (exclude system messages)
|
|
107
|
+
for (const msg of conversationMessages) {
|
|
108
|
+
if (msg.role !== 'system' && msg.role !== 'tool') {
|
|
109
|
+
input.push({
|
|
110
|
+
type: 'message',
|
|
111
|
+
role: msg.role,
|
|
112
|
+
content: [{
|
|
113
|
+
type: msg.role === 'user' ? 'input_text' : 'output_text',
|
|
114
|
+
text: msg.content,
|
|
115
|
+
}],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Add compression request as final user message
|
|
120
|
+
input.push({
|
|
121
|
+
type: 'message',
|
|
122
|
+
role: 'user',
|
|
123
|
+
content: [{
|
|
124
|
+
type: 'input_text',
|
|
125
|
+
text: COMPRESSION_PROMPT,
|
|
126
|
+
}],
|
|
127
|
+
});
|
|
128
|
+
// Build request payload (no tools for compression)
|
|
129
|
+
const requestPayload = {
|
|
130
|
+
model: modelName,
|
|
131
|
+
instructions,
|
|
132
|
+
input,
|
|
133
|
+
stream: true,
|
|
134
|
+
};
|
|
135
|
+
// Log request payload
|
|
136
|
+
console.log('[ContextCompressor] Responses API Request:', JSON.stringify(requestPayload, null, 2));
|
|
137
|
+
// Use streaming to avoid timeout
|
|
138
|
+
const stream = await client.responses.create(requestPayload, {
|
|
139
|
+
headers: customHeaders,
|
|
140
|
+
});
|
|
141
|
+
let summary = '';
|
|
142
|
+
let usage = {
|
|
143
|
+
prompt_tokens: 0,
|
|
144
|
+
completion_tokens: 0,
|
|
145
|
+
total_tokens: 0,
|
|
146
|
+
};
|
|
147
|
+
for await (const chunk of stream) {
|
|
148
|
+
const eventType = chunk.type;
|
|
149
|
+
// Handle text content delta
|
|
150
|
+
if (eventType === 'response.output_text.delta') {
|
|
151
|
+
const delta = chunk.delta;
|
|
152
|
+
if (delta) {
|
|
153
|
+
summary += delta;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Handle usage info
|
|
157
|
+
if (eventType === 'response.done') {
|
|
158
|
+
const response = chunk.response;
|
|
159
|
+
if (response?.usage) {
|
|
160
|
+
usage = {
|
|
161
|
+
prompt_tokens: response.usage.input_tokens || 0,
|
|
162
|
+
completion_tokens: response.usage.output_tokens || 0,
|
|
163
|
+
total_tokens: (response.usage.input_tokens || 0) + (response.usage.output_tokens || 0),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!summary) {
|
|
169
|
+
throw new Error('Failed to generate summary from compact model (Responses API)');
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
summary,
|
|
173
|
+
usage,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Compress context using Gemini API
|
|
178
|
+
*/
|
|
179
|
+
async function compressWithGemini(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
|
|
180
|
+
const clientConfig = {
|
|
181
|
+
apiKey,
|
|
182
|
+
};
|
|
183
|
+
const customHeaders = getCustomHeaders();
|
|
184
|
+
// Support custom baseUrl and headers for proxy servers
|
|
185
|
+
if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
|
|
186
|
+
clientConfig.httpOptions = {
|
|
187
|
+
baseUrl,
|
|
188
|
+
headers: {
|
|
189
|
+
'x-goog-api-key': apiKey,
|
|
190
|
+
...customHeaders,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
else if (Object.keys(customHeaders).length > 0) {
|
|
195
|
+
clientConfig.httpOptions = {
|
|
196
|
+
headers: customHeaders,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const client = new GoogleGenAI(clientConfig);
|
|
200
|
+
// Build system instruction
|
|
201
|
+
const systemInstruction = systemPrompt || SYSTEM_PROMPT;
|
|
202
|
+
// Build contents array with conversation history
|
|
203
|
+
const contents = [];
|
|
204
|
+
// If custom system prompt exists, add default as first user message
|
|
205
|
+
if (systemPrompt) {
|
|
206
|
+
contents.push({
|
|
207
|
+
role: 'user',
|
|
208
|
+
parts: [{ text: SYSTEM_PROMPT }],
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Add all conversation history (exclude system messages)
|
|
212
|
+
for (const msg of conversationMessages) {
|
|
213
|
+
if (msg.role !== 'system' && msg.role !== 'tool') {
|
|
214
|
+
contents.push({
|
|
215
|
+
role: msg.role === 'assistant' ? 'model' : 'user',
|
|
216
|
+
parts: [{ text: msg.content }],
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Add compression request as final user message
|
|
221
|
+
contents.push({
|
|
222
|
+
role: 'user',
|
|
223
|
+
parts: [{
|
|
224
|
+
text: COMPRESSION_PROMPT,
|
|
225
|
+
}],
|
|
226
|
+
});
|
|
227
|
+
const requestConfig = {
|
|
228
|
+
model: modelName,
|
|
229
|
+
systemInstruction,
|
|
230
|
+
contents,
|
|
231
|
+
};
|
|
232
|
+
// Log request payload
|
|
233
|
+
console.log('[ContextCompressor] Gemini Request:', JSON.stringify(requestConfig, null, 2));
|
|
234
|
+
// Use streaming to avoid timeout
|
|
235
|
+
const stream = await client.models.generateContentStream(requestConfig);
|
|
236
|
+
let summary = '';
|
|
237
|
+
let usage = {
|
|
238
|
+
prompt_tokens: 0,
|
|
239
|
+
completion_tokens: 0,
|
|
240
|
+
total_tokens: 0,
|
|
241
|
+
};
|
|
242
|
+
for await (const chunk of stream) {
|
|
243
|
+
if (chunk.text) {
|
|
244
|
+
summary += chunk.text;
|
|
245
|
+
}
|
|
246
|
+
// Collect usage info
|
|
247
|
+
if (chunk.usageMetadata) {
|
|
248
|
+
usage = {
|
|
249
|
+
prompt_tokens: chunk.usageMetadata.promptTokenCount || 0,
|
|
250
|
+
completion_tokens: chunk.usageMetadata.candidatesTokenCount || 0,
|
|
251
|
+
total_tokens: chunk.usageMetadata.totalTokenCount || 0,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!summary) {
|
|
256
|
+
throw new Error('Failed to generate summary from Gemini model');
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
summary,
|
|
260
|
+
usage,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Compress context using Anthropic API
|
|
265
|
+
*/
|
|
266
|
+
async function compressWithAnthropic(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
|
|
267
|
+
const clientConfig = {
|
|
268
|
+
apiKey,
|
|
269
|
+
};
|
|
270
|
+
if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
|
|
271
|
+
clientConfig.baseURL = baseUrl;
|
|
272
|
+
}
|
|
273
|
+
const customHeaders = getCustomHeaders();
|
|
274
|
+
clientConfig.defaultHeaders = {
|
|
275
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
276
|
+
...customHeaders,
|
|
277
|
+
};
|
|
278
|
+
const client = new Anthropic(clientConfig);
|
|
279
|
+
// Build messages array with conversation history
|
|
280
|
+
const messages = [];
|
|
281
|
+
// If custom system prompt exists, add default as first user message
|
|
282
|
+
if (systemPrompt) {
|
|
283
|
+
messages.push({ role: 'user', content: SYSTEM_PROMPT });
|
|
284
|
+
}
|
|
285
|
+
// Add all conversation history (exclude system messages)
|
|
286
|
+
for (const msg of conversationMessages) {
|
|
287
|
+
if (msg.role !== 'system' && msg.role !== 'tool') {
|
|
288
|
+
messages.push({
|
|
289
|
+
role: msg.role,
|
|
290
|
+
content: msg.content,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Add compression request as final user message
|
|
295
|
+
messages.push({
|
|
296
|
+
role: 'user',
|
|
297
|
+
content: COMPRESSION_PROMPT,
|
|
298
|
+
});
|
|
299
|
+
// Anthropic uses system parameter separately
|
|
300
|
+
const systemParam = systemPrompt || SYSTEM_PROMPT;
|
|
301
|
+
// Build request payload (no tools for compression)
|
|
302
|
+
const requestPayload = {
|
|
303
|
+
model: modelName,
|
|
304
|
+
max_tokens: 4096,
|
|
305
|
+
system: systemParam,
|
|
306
|
+
messages,
|
|
307
|
+
};
|
|
308
|
+
// Log request payload
|
|
309
|
+
console.log('[ContextCompressor] Anthropic Request:', JSON.stringify(requestPayload, null, 2));
|
|
310
|
+
// Use streaming to avoid timeout
|
|
311
|
+
const stream = await client.messages.stream(requestPayload);
|
|
312
|
+
let summary = '';
|
|
313
|
+
let usage = {
|
|
314
|
+
prompt_tokens: 0,
|
|
315
|
+
completion_tokens: 0,
|
|
316
|
+
total_tokens: 0,
|
|
317
|
+
};
|
|
318
|
+
for await (const event of stream) {
|
|
319
|
+
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
320
|
+
summary += event.delta.text;
|
|
321
|
+
}
|
|
322
|
+
// Collect usage info from message_stop event
|
|
323
|
+
if (event.type === 'message_stop') {
|
|
324
|
+
const finalMessage = await stream.finalMessage();
|
|
325
|
+
usage = {
|
|
326
|
+
prompt_tokens: finalMessage.usage.input_tokens,
|
|
327
|
+
completion_tokens: finalMessage.usage.output_tokens,
|
|
328
|
+
total_tokens: finalMessage.usage.input_tokens + finalMessage.usage.output_tokens,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (!summary) {
|
|
333
|
+
throw new Error('Failed to generate summary from Anthropic model');
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
summary,
|
|
337
|
+
usage,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
3
340
|
/**
|
|
4
341
|
* Compress conversation history using the compact model
|
|
5
342
|
* @param messages - Array of messages to compress
|
|
@@ -8,57 +345,34 @@ import { getOpenAiConfig } from './apiConfig.js';
|
|
|
8
345
|
export async function compressContext(messages) {
|
|
9
346
|
const config = getOpenAiConfig();
|
|
10
347
|
// Check if compact model is configured
|
|
11
|
-
if (!config.compactModel || !config.compactModel.
|
|
12
|
-
throw new Error('Compact model not configured. Please configure it in Model Settings.');
|
|
348
|
+
if (!config.compactModel || !config.compactModel.modelName) {
|
|
349
|
+
throw new Error('Compact model not configured. Please configure it in API & Model Settings.');
|
|
13
350
|
}
|
|
14
|
-
//
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return `${role}: ${msg.content}`;
|
|
25
|
-
})
|
|
26
|
-
.join('\n\n');
|
|
27
|
-
// Create compression prompt
|
|
28
|
-
const compressionPrompt = `Please summarize the following conversation history in a concise way, preserving all important context, decisions, and key information. The summary should be detailed enough to continue the conversation seamlessly.
|
|
29
|
-
|
|
30
|
-
Conversation:
|
|
31
|
-
${conversationText}
|
|
32
|
-
|
|
33
|
-
Summary:`;
|
|
351
|
+
// Use shared API credentials
|
|
352
|
+
const baseUrl = config.baseUrl;
|
|
353
|
+
const apiKey = config.apiKey;
|
|
354
|
+
const modelName = config.compactModel.modelName;
|
|
355
|
+
const requestMethod = config.requestMethod;
|
|
356
|
+
if (!baseUrl || !apiKey) {
|
|
357
|
+
throw new Error('API configuration incomplete. Please configure Base URL and API Key.');
|
|
358
|
+
}
|
|
359
|
+
// Get custom system prompt if configured
|
|
360
|
+
const customSystemPrompt = getCustomSystemPrompt();
|
|
34
361
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
362
|
+
// Choose compression method based on request method
|
|
363
|
+
switch (requestMethod) {
|
|
364
|
+
case 'gemini':
|
|
365
|
+
return await compressWithGemini(baseUrl, apiKey, modelName, messages, customSystemPrompt || null);
|
|
366
|
+
case 'anthropic':
|
|
367
|
+
return await compressWithAnthropic(baseUrl, apiKey, modelName, messages, customSystemPrompt || null);
|
|
368
|
+
case 'responses':
|
|
369
|
+
// OpenAI Responses API
|
|
370
|
+
return await compressWithResponses(baseUrl, apiKey, modelName, messages, customSystemPrompt || null);
|
|
371
|
+
case 'chat':
|
|
372
|
+
default:
|
|
373
|
+
// OpenAI Chat Completions API
|
|
374
|
+
return await compressWithChatCompletions(baseUrl, apiKey, modelName, messages, customSystemPrompt || null);
|
|
47
375
|
}
|
|
48
|
-
// Extract usage information
|
|
49
|
-
const usage = response.usage || {
|
|
50
|
-
prompt_tokens: 0,
|
|
51
|
-
completion_tokens: 0,
|
|
52
|
-
total_tokens: 0
|
|
53
|
-
};
|
|
54
|
-
return {
|
|
55
|
-
summary,
|
|
56
|
-
usage: {
|
|
57
|
-
prompt_tokens: usage.prompt_tokens,
|
|
58
|
-
completion_tokens: usage.completion_tokens,
|
|
59
|
-
total_tokens: usage.total_tokens
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
376
|
}
|
|
63
377
|
catch (error) {
|
|
64
378
|
if (error instanceof Error) {
|
package/dist/utils/retryUtils.js
CHANGED
|
@@ -64,6 +64,12 @@ function isRetriableError(error) {
|
|
|
64
64
|
errorMessage.includes('unavailable')) {
|
|
65
65
|
return true;
|
|
66
66
|
}
|
|
67
|
+
// Connection terminated by server
|
|
68
|
+
if (errorMessage.includes('terminated') ||
|
|
69
|
+
errorMessage.includes('connection reset') ||
|
|
70
|
+
errorMessage.includes('socket hang up')) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
67
73
|
return false;
|
|
68
74
|
}
|
|
69
75
|
/**
|
package/package.json
CHANGED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
import Gradient from 'ink-gradient';
|
|
4
|
-
import { Select, Alert } from '@inkjs/ui';
|
|
5
|
-
import TextInput from 'ink-text-input';
|
|
6
|
-
import { getOpenAiConfig, updateOpenAiConfig, validateApiConfig, } from '../../utils/apiConfig.js';
|
|
7
|
-
export default function ApiConfigScreen({ onBack, onSave, inlineMode = false }) {
|
|
8
|
-
const [baseUrl, setBaseUrl] = useState('');
|
|
9
|
-
const [apiKey, setApiKey] = useState('');
|
|
10
|
-
const [requestMethod, setRequestMethod] = useState('chat');
|
|
11
|
-
const [anthropicBeta, setAnthropicBeta] = useState(false);
|
|
12
|
-
const [currentField, setCurrentField] = useState('baseUrl');
|
|
13
|
-
const [errors, setErrors] = useState([]);
|
|
14
|
-
const [isEditing, setIsEditing] = useState(false);
|
|
15
|
-
const requestMethodOptions = [
|
|
16
|
-
{
|
|
17
|
-
label: 'Chat Completions - Modern chat API (GPT-4, GPT-3.5-turbo)',
|
|
18
|
-
value: 'chat',
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
label: 'Responses - New responses API (2025, with built-in tools)',
|
|
22
|
-
value: 'responses',
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
label: 'Gemini - Google Gemini API',
|
|
26
|
-
value: 'gemini',
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
label: 'Anthropic - Claude API',
|
|
30
|
-
value: 'anthropic',
|
|
31
|
-
},
|
|
32
|
-
];
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
const config = getOpenAiConfig();
|
|
35
|
-
setBaseUrl(config.baseUrl);
|
|
36
|
-
setApiKey(config.apiKey);
|
|
37
|
-
setRequestMethod(config.requestMethod || 'chat');
|
|
38
|
-
setAnthropicBeta(config.anthropicBeta || false);
|
|
39
|
-
}, []);
|
|
40
|
-
useInput((input, key) => {
|
|
41
|
-
// Allow Escape key to exit Select component without changes
|
|
42
|
-
if (isEditing && currentField === 'requestMethod' && key.escape) {
|
|
43
|
-
setIsEditing(false);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
// Don't handle other input when Select component is active
|
|
47
|
-
if (isEditing && currentField === 'requestMethod') {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
// Handle save/exit globally
|
|
51
|
-
if (input === 's' && (key.ctrl || key.meta)) {
|
|
52
|
-
const validationErrors = validateApiConfig({ baseUrl, apiKey, requestMethod });
|
|
53
|
-
if (validationErrors.length === 0) {
|
|
54
|
-
updateOpenAiConfig({ baseUrl, apiKey, requestMethod, anthropicBeta });
|
|
55
|
-
setErrors([]);
|
|
56
|
-
onSave();
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
setErrors(validationErrors);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
else if (key.escape) {
|
|
63
|
-
const validationErrors = validateApiConfig({ baseUrl, apiKey, requestMethod });
|
|
64
|
-
if (validationErrors.length === 0) {
|
|
65
|
-
updateOpenAiConfig({ baseUrl, apiKey, requestMethod, anthropicBeta });
|
|
66
|
-
setErrors([]);
|
|
67
|
-
}
|
|
68
|
-
onBack();
|
|
69
|
-
}
|
|
70
|
-
else if (key.return) {
|
|
71
|
-
if (isEditing) {
|
|
72
|
-
// Exit edit mode, return to navigation
|
|
73
|
-
setIsEditing(false);
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
// Enter edit mode for current field (toggle for checkbox)
|
|
77
|
-
if (currentField === 'anthropicBeta') {
|
|
78
|
-
setAnthropicBeta(!anthropicBeta);
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
setIsEditing(true);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
else if (!isEditing && key.upArrow) {
|
|
86
|
-
if (currentField === 'apiKey') {
|
|
87
|
-
setCurrentField('baseUrl');
|
|
88
|
-
}
|
|
89
|
-
else if (currentField === 'requestMethod') {
|
|
90
|
-
setCurrentField('apiKey');
|
|
91
|
-
}
|
|
92
|
-
else if (currentField === 'anthropicBeta') {
|
|
93
|
-
setCurrentField('requestMethod');
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
else if (!isEditing && key.downArrow) {
|
|
97
|
-
if (currentField === 'baseUrl') {
|
|
98
|
-
setCurrentField('apiKey');
|
|
99
|
-
}
|
|
100
|
-
else if (currentField === 'apiKey') {
|
|
101
|
-
setCurrentField('requestMethod');
|
|
102
|
-
}
|
|
103
|
-
else if (currentField === 'requestMethod') {
|
|
104
|
-
setCurrentField('anthropicBeta');
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
109
|
-
!inlineMode && (React.createElement(Box, { marginBottom: 2, borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1 },
|
|
110
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
111
|
-
React.createElement(Gradient, { name: "rainbow" }, "OpenAI API Configuration"),
|
|
112
|
-
React.createElement(Text, { color: "gray", dimColor: true }, "Configure your OpenAI API settings")))),
|
|
113
|
-
React.createElement(Box, { flexDirection: "column", marginBottom: 2 },
|
|
114
|
-
React.createElement(Box, { marginBottom: 1 },
|
|
115
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
116
|
-
React.createElement(Text, { color: currentField === 'baseUrl' ? 'green' : 'white' },
|
|
117
|
-
currentField === 'baseUrl' ? '❯ ' : ' ',
|
|
118
|
-
"Base URL:"),
|
|
119
|
-
currentField === 'baseUrl' && isEditing && (React.createElement(Box, { marginLeft: 3 },
|
|
120
|
-
React.createElement(TextInput, { value: baseUrl, onChange: setBaseUrl, placeholder: "https://api.openai.com/v1" }))),
|
|
121
|
-
(!isEditing || currentField !== 'baseUrl') && (React.createElement(Box, { marginLeft: 3 },
|
|
122
|
-
React.createElement(Text, { color: "gray" }, baseUrl || 'Not set'))))),
|
|
123
|
-
React.createElement(Box, { marginBottom: 1 },
|
|
124
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
125
|
-
React.createElement(Text, { color: currentField === 'apiKey' ? 'green' : 'white' },
|
|
126
|
-
currentField === 'apiKey' ? '❯ ' : ' ',
|
|
127
|
-
"API Key:"),
|
|
128
|
-
currentField === 'apiKey' && isEditing && (React.createElement(Box, { marginLeft: 3 },
|
|
129
|
-
React.createElement(TextInput, { value: apiKey, onChange: setApiKey, placeholder: "sk-...", mask: "*" }))),
|
|
130
|
-
(!isEditing || currentField !== 'apiKey') && (React.createElement(Box, { marginLeft: 3 },
|
|
131
|
-
React.createElement(Text, { color: "gray" }, apiKey ? '*'.repeat(Math.min(apiKey.length, 20)) : 'Not set'))))),
|
|
132
|
-
React.createElement(Box, { marginBottom: 1 },
|
|
133
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
134
|
-
React.createElement(Text, { color: currentField === 'requestMethod' ? 'green' : 'white' },
|
|
135
|
-
currentField === 'requestMethod' ? '❯ ' : ' ',
|
|
136
|
-
"Request Method:"),
|
|
137
|
-
currentField === 'requestMethod' && isEditing && (React.createElement(Box, { marginLeft: 3 },
|
|
138
|
-
React.createElement(Select, { options: requestMethodOptions, defaultValue: requestMethod, onChange: (value) => {
|
|
139
|
-
setRequestMethod(value);
|
|
140
|
-
setIsEditing(false); // Auto exit edit mode after selection
|
|
141
|
-
} }))),
|
|
142
|
-
(!isEditing || currentField !== 'requestMethod') && (React.createElement(Box, { marginLeft: 3 },
|
|
143
|
-
React.createElement(Text, { color: "gray" }, requestMethodOptions.find(opt => opt.value === requestMethod)?.label || 'Not set'))))),
|
|
144
|
-
React.createElement(Box, { marginBottom: 1 },
|
|
145
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
146
|
-
React.createElement(Text, { color: currentField === 'anthropicBeta' ? 'green' : 'white' },
|
|
147
|
-
currentField === 'anthropicBeta' ? '❯ ' : ' ',
|
|
148
|
-
"Anthropic Beta (for Claude API):"),
|
|
149
|
-
React.createElement(Box, { marginLeft: 3 },
|
|
150
|
-
React.createElement(Text, { color: "gray" },
|
|
151
|
-
anthropicBeta ? '☑ Enabled' : '☐ Disabled',
|
|
152
|
-
" (Press Enter to toggle)"))))),
|
|
153
|
-
errors.length > 0 && (React.createElement(Box, { flexDirection: "column", marginBottom: 2 },
|
|
154
|
-
React.createElement(Text, { color: "red", bold: true }, "Errors:"),
|
|
155
|
-
errors.map((error, index) => (React.createElement(Text, { key: index, color: "red" },
|
|
156
|
-
"\u2022 ",
|
|
157
|
-
error))))),
|
|
158
|
-
React.createElement(Box, { flexDirection: "column" }, isEditing ? (React.createElement(React.Fragment, null,
|
|
159
|
-
React.createElement(Alert, { variant: "info" }, "Editing mode: Press Enter to save and exit editing (Make your changes and press Enter when done)"))) : (React.createElement(React.Fragment, null,
|
|
160
|
-
React.createElement(Alert, { variant: "info" }, "Use \u2191\u2193 to navigate between fields, press Enter to edit, and press Ctrl+S or Esc to save and return"))))));
|
|
161
|
-
}
|