snow-ai 0.1.12
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 +29 -0
- package/dist/api/chat.js +88 -0
- package/dist/api/models.d.ts +12 -0
- package/dist/api/models.js +40 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +47 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +19 -0
- package/dist/constants/index.d.ts +18 -0
- package/dist/constants/index.js +18 -0
- package/dist/hooks/useGlobalExit.d.ts +5 -0
- package/dist/hooks/useGlobalExit.js +32 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.js +1 -0
- package/dist/ui/components/ChatInput.d.ts +9 -0
- package/dist/ui/components/ChatInput.js +206 -0
- package/dist/ui/components/CommandPanel.d.ts +13 -0
- package/dist/ui/components/CommandPanel.js +22 -0
- package/dist/ui/components/Menu.d.ts +14 -0
- package/dist/ui/components/Menu.js +32 -0
- package/dist/ui/components/MessageList.d.ts +15 -0
- package/dist/ui/components/MessageList.js +16 -0
- package/dist/ui/components/PendingMessages.d.ts +6 -0
- package/dist/ui/components/PendingMessages.js +19 -0
- package/dist/ui/pages/ApiConfigScreen.d.ts +7 -0
- package/dist/ui/pages/ApiConfigScreen.js +126 -0
- package/dist/ui/pages/ChatScreen.d.ts +5 -0
- package/dist/ui/pages/ChatScreen.js +287 -0
- package/dist/ui/pages/ModelConfigScreen.d.ts +7 -0
- package/dist/ui/pages/ModelConfigScreen.js +239 -0
- package/dist/ui/pages/WelcomeScreen.d.ts +7 -0
- package/dist/ui/pages/WelcomeScreen.js +48 -0
- package/dist/utils/apiConfig.d.ts +17 -0
- package/dist/utils/apiConfig.js +86 -0
- package/dist/utils/commandExecutor.d.ts +11 -0
- package/dist/utils/commandExecutor.js +26 -0
- package/dist/utils/commands/clear.d.ts +2 -0
- package/dist/utils/commands/clear.js +12 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/textBuffer.d.ts +52 -0
- package/dist/utils/textBuffer.js +310 -0
- package/dist/utils/textUtils.d.ts +33 -0
- package/dist/utils/textUtils.js +83 -0
- package/package.json +86 -0
- package/readme.md +9 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export default function MessageList({ messages, animationFrame, maxMessages = 6 }) {
|
|
4
|
+
if (messages.length === 0) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
return (React.createElement(Box, { marginBottom: 1, flexDirection: "column", paddingX: 1, paddingY: 1 }, messages.slice(-maxMessages).map((message, index) => (React.createElement(Box, { key: index, marginLeft: 1 },
|
|
8
|
+
React.createElement(Text, { color: message.role === 'user' ? 'blue' :
|
|
9
|
+
message.role === 'command' ? 'gray' :
|
|
10
|
+
message.streaming ? ['#FF6EBF', 'green', 'blue', 'cyan', '#B588F8'][animationFrame] : 'cyan', bold: true }, message.role === 'user' ? '⛇' : message.role === 'command' ? '⌘' : '❆'),
|
|
11
|
+
React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" }, message.role === 'command' ? (React.createElement(Text, { color: "gray" },
|
|
12
|
+
"\u2514\u2500 ",
|
|
13
|
+
message.commandName)) : (React.createElement(React.Fragment, null,
|
|
14
|
+
React.createElement(Text, { color: message.role === 'user' ? 'gray' : '' }, message.content),
|
|
15
|
+
message.discontinued && (React.createElement(Text, { color: "red", bold: true }, "\u2514\u2500 user discontinue"))))))))));
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export default function PendingMessages({ pendingMessages }) {
|
|
4
|
+
if (pendingMessages.length === 0) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
return (React.createElement(Box, { marginBottom: 1, flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1 },
|
|
8
|
+
React.createElement(Text, { color: "yellow", bold: true },
|
|
9
|
+
"\u2B11 Pending Messages (",
|
|
10
|
+
pendingMessages.length,
|
|
11
|
+
")"),
|
|
12
|
+
pendingMessages.map((message, index) => (React.createElement(Box, { key: index, marginLeft: 1, marginY: 0 },
|
|
13
|
+
React.createElement(Text, { color: "blue", bold: true },
|
|
14
|
+
index + 1,
|
|
15
|
+
"."),
|
|
16
|
+
React.createElement(Box, { marginLeft: 1 },
|
|
17
|
+
React.createElement(Text, { color: "gray" }, message.length > 60 ? `${message.substring(0, 60)}...` : message))))),
|
|
18
|
+
React.createElement(Text, { color: "yellow", dimColor: true }, "Will be sent when AI finishes responding")));
|
|
19
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
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 }) {
|
|
8
|
+
const [baseUrl, setBaseUrl] = useState('');
|
|
9
|
+
const [apiKey, setApiKey] = useState('');
|
|
10
|
+
const [requestMethod, setRequestMethod] = useState('chat');
|
|
11
|
+
const [currentField, setCurrentField] = useState('baseUrl');
|
|
12
|
+
const [errors, setErrors] = useState([]);
|
|
13
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
14
|
+
const requestMethodOptions = [
|
|
15
|
+
{
|
|
16
|
+
label: 'Chat Completions - Modern chat API (GPT-4, GPT-3.5-turbo)',
|
|
17
|
+
value: 'chat',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: 'Responses - New responses API (2025, with built-in tools)',
|
|
21
|
+
value: 'responses',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const config = getOpenAiConfig();
|
|
26
|
+
setBaseUrl(config.baseUrl);
|
|
27
|
+
setApiKey(config.apiKey);
|
|
28
|
+
setRequestMethod(config.requestMethod || 'chat');
|
|
29
|
+
}, []);
|
|
30
|
+
useInput((input, key) => {
|
|
31
|
+
// Don't handle input when Select component is active
|
|
32
|
+
if (isEditing && currentField === 'requestMethod') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Handle save/exit globally
|
|
36
|
+
if (input === 's' && (key.ctrl || key.meta)) {
|
|
37
|
+
const validationErrors = validateApiConfig({ baseUrl, apiKey, requestMethod });
|
|
38
|
+
if (validationErrors.length === 0) {
|
|
39
|
+
updateOpenAiConfig({ baseUrl, apiKey, requestMethod });
|
|
40
|
+
setErrors([]);
|
|
41
|
+
onSave();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
setErrors(validationErrors);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (key.escape) {
|
|
48
|
+
const validationErrors = validateApiConfig({ baseUrl, apiKey, requestMethod });
|
|
49
|
+
if (validationErrors.length === 0) {
|
|
50
|
+
updateOpenAiConfig({ baseUrl, apiKey, requestMethod });
|
|
51
|
+
setErrors([]);
|
|
52
|
+
}
|
|
53
|
+
onBack();
|
|
54
|
+
}
|
|
55
|
+
else if (key.return) {
|
|
56
|
+
if (isEditing) {
|
|
57
|
+
// Exit edit mode, return to navigation
|
|
58
|
+
setIsEditing(false);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Enter edit mode for current field
|
|
62
|
+
setIsEditing(true);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (!isEditing && key.upArrow) {
|
|
66
|
+
if (currentField === 'apiKey') {
|
|
67
|
+
setCurrentField('baseUrl');
|
|
68
|
+
}
|
|
69
|
+
else if (currentField === 'requestMethod') {
|
|
70
|
+
setCurrentField('apiKey');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (!isEditing && key.downArrow) {
|
|
74
|
+
if (currentField === 'baseUrl') {
|
|
75
|
+
setCurrentField('apiKey');
|
|
76
|
+
}
|
|
77
|
+
else if (currentField === 'apiKey') {
|
|
78
|
+
setCurrentField('requestMethod');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
83
|
+
React.createElement(Box, { marginBottom: 2, borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1 },
|
|
84
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
85
|
+
React.createElement(Gradient, { name: "rainbow" }, "OpenAI API Configuration"),
|
|
86
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "Configure your OpenAI API settings"))),
|
|
87
|
+
React.createElement(Box, { flexDirection: "column", marginBottom: 2 },
|
|
88
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
89
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
90
|
+
React.createElement(Text, { color: currentField === 'baseUrl' ? 'green' : 'white' },
|
|
91
|
+
currentField === 'baseUrl' ? '➣ ' : ' ',
|
|
92
|
+
"Base URL:"),
|
|
93
|
+
currentField === 'baseUrl' && isEditing && (React.createElement(Box, { marginLeft: 3 },
|
|
94
|
+
React.createElement(TextInput, { value: baseUrl, onChange: setBaseUrl, placeholder: "https://api.openai.com/v1" }))),
|
|
95
|
+
(!isEditing || currentField !== 'baseUrl') && (React.createElement(Box, { marginLeft: 3 },
|
|
96
|
+
React.createElement(Text, { color: "gray" }, baseUrl || 'Not set'))))),
|
|
97
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
98
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
99
|
+
React.createElement(Text, { color: currentField === 'apiKey' ? 'green' : 'white' },
|
|
100
|
+
currentField === 'apiKey' ? '➣ ' : ' ',
|
|
101
|
+
"API Key:"),
|
|
102
|
+
currentField === 'apiKey' && isEditing && (React.createElement(Box, { marginLeft: 3 },
|
|
103
|
+
React.createElement(TextInput, { value: apiKey, onChange: setApiKey, placeholder: "sk-...", mask: "*" }))),
|
|
104
|
+
(!isEditing || currentField !== 'apiKey') && (React.createElement(Box, { marginLeft: 3 },
|
|
105
|
+
React.createElement(Text, { color: "gray" }, apiKey ? '*'.repeat(Math.min(apiKey.length, 20)) : 'Not set'))))),
|
|
106
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
107
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
108
|
+
React.createElement(Text, { color: currentField === 'requestMethod' ? 'green' : 'white' },
|
|
109
|
+
currentField === 'requestMethod' ? '➣ ' : ' ',
|
|
110
|
+
"Request Method:"),
|
|
111
|
+
currentField === 'requestMethod' && isEditing && (React.createElement(Box, { marginLeft: 3 },
|
|
112
|
+
React.createElement(Select, { options: requestMethodOptions, defaultValue: requestMethod, onChange: (value) => {
|
|
113
|
+
setRequestMethod(value);
|
|
114
|
+
setIsEditing(false); // Auto exit edit mode after selection
|
|
115
|
+
} }))),
|
|
116
|
+
(!isEditing || currentField !== 'requestMethod') && (React.createElement(Box, { marginLeft: 3 },
|
|
117
|
+
React.createElement(Text, { color: "gray" }, requestMethodOptions.find(opt => opt.value === requestMethod)?.label || 'Not set')))))),
|
|
118
|
+
errors.length > 0 && (React.createElement(Box, { flexDirection: "column", marginBottom: 2 },
|
|
119
|
+
React.createElement(Text, { color: "red", bold: true }, "Errors:"),
|
|
120
|
+
errors.map((error, index) => (React.createElement(Text, { key: index, color: "red" },
|
|
121
|
+
"\u2022 ",
|
|
122
|
+
error))))),
|
|
123
|
+
React.createElement(Box, { flexDirection: "column" }, isEditing ? (React.createElement(React.Fragment, null,
|
|
124
|
+
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,
|
|
125
|
+
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"))))));
|
|
126
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import Gradient from 'ink-gradient';
|
|
4
|
+
import ChatInput from '../components/ChatInput.js';
|
|
5
|
+
import MessageList from '../components/MessageList.js';
|
|
6
|
+
import PendingMessages from '../components/PendingMessages.js';
|
|
7
|
+
import { createStreamingChatCompletion } from '../../api/chat.js';
|
|
8
|
+
import { getOpenAiConfig } from '../../utils/apiConfig.js';
|
|
9
|
+
// Import clear command to register it
|
|
10
|
+
import '../../utils/commands/clear.js';
|
|
11
|
+
export default function ChatScreen({}) {
|
|
12
|
+
const [messages, setMessages] = useState([]);
|
|
13
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
14
|
+
const [animationFrame, setAnimationFrame] = useState(0);
|
|
15
|
+
const [abortController, setAbortController] = useState(null);
|
|
16
|
+
const [pendingMessages, setPendingMessages] = useState([]);
|
|
17
|
+
// Animation for streaming indicator
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!isStreaming)
|
|
20
|
+
return;
|
|
21
|
+
const interval = setInterval(() => {
|
|
22
|
+
setAnimationFrame(prev => (prev + 1) % 5);
|
|
23
|
+
}, 300);
|
|
24
|
+
return () => clearInterval(interval);
|
|
25
|
+
}, [isStreaming]);
|
|
26
|
+
// Auto-send pending messages when streaming stops
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!isStreaming && pendingMessages.length > 0) {
|
|
29
|
+
// Use setTimeout to ensure state updates are complete
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
processPendingMessages();
|
|
32
|
+
}, 100);
|
|
33
|
+
return () => clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}, [isStreaming, pendingMessages.length]);
|
|
37
|
+
// ESC key handler to interrupt streaming
|
|
38
|
+
useInput((_, key) => {
|
|
39
|
+
if (key.escape && isStreaming && abortController) {
|
|
40
|
+
abortController.abort();
|
|
41
|
+
setMessages(prev => {
|
|
42
|
+
const newMessages = [...prev];
|
|
43
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
44
|
+
if (lastMessage && lastMessage.streaming) {
|
|
45
|
+
lastMessage.streaming = false;
|
|
46
|
+
lastMessage.discontinued = true;
|
|
47
|
+
}
|
|
48
|
+
return newMessages;
|
|
49
|
+
});
|
|
50
|
+
// Reset streaming state, useEffect will handle pending messages
|
|
51
|
+
setIsStreaming(false);
|
|
52
|
+
setAbortController(null);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const handleCommandExecution = (commandName, result) => {
|
|
56
|
+
if (result.success && result.action === 'clear') {
|
|
57
|
+
// Clear all messages
|
|
58
|
+
setMessages([]);
|
|
59
|
+
// Add command execution feedback
|
|
60
|
+
const commandMessage = {
|
|
61
|
+
role: 'command',
|
|
62
|
+
content: '',
|
|
63
|
+
commandName: commandName
|
|
64
|
+
};
|
|
65
|
+
setMessages([commandMessage]);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const handleMessageSubmit = async (message) => {
|
|
69
|
+
// If streaming, add to pending messages instead of sending immediately
|
|
70
|
+
if (isStreaming) {
|
|
71
|
+
setPendingMessages(prev => [...prev, message]);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Process the message normally
|
|
75
|
+
await processMessage(message);
|
|
76
|
+
};
|
|
77
|
+
const processMessage = async (message) => {
|
|
78
|
+
const userMessage = { role: 'user', content: message };
|
|
79
|
+
setMessages(prev => [...prev, userMessage]);
|
|
80
|
+
setIsStreaming(true);
|
|
81
|
+
// Create new abort controller for this request
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
setAbortController(controller);
|
|
84
|
+
const assistantMessage = { role: 'assistant', content: '', streaming: true };
|
|
85
|
+
setMessages(prev => [...prev, assistantMessage]);
|
|
86
|
+
try {
|
|
87
|
+
const config = getOpenAiConfig();
|
|
88
|
+
const model = config.advancedModel || 'gpt-4.1';
|
|
89
|
+
// Check if request method is responses (not yet implemented)
|
|
90
|
+
if (config.requestMethod === 'responses') {
|
|
91
|
+
setMessages(prev => {
|
|
92
|
+
const newMessages = [...prev];
|
|
93
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
94
|
+
if (lastMessage) {
|
|
95
|
+
lastMessage.content = 'Responses API is not yet implemented. Please use "Chat Completions" method in API settings.';
|
|
96
|
+
lastMessage.streaming = false;
|
|
97
|
+
}
|
|
98
|
+
return newMessages;
|
|
99
|
+
});
|
|
100
|
+
// Don't return here, let it fall through to finally block
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const chatMessages = [
|
|
104
|
+
{ role: 'system', content: 'You are a helpful coding assistant.' },
|
|
105
|
+
...messages.filter(msg => msg.role !== 'command').map(msg => ({ role: msg.role, content: msg.content })),
|
|
106
|
+
{ role: 'user', content: message }
|
|
107
|
+
];
|
|
108
|
+
let fullResponse = '';
|
|
109
|
+
let currentLine = '';
|
|
110
|
+
for await (const chunk of createStreamingChatCompletion({
|
|
111
|
+
model,
|
|
112
|
+
messages: chatMessages,
|
|
113
|
+
temperature: 0
|
|
114
|
+
}, controller.signal)) {
|
|
115
|
+
if (controller.signal.aborted)
|
|
116
|
+
break;
|
|
117
|
+
currentLine += chunk;
|
|
118
|
+
// Check if we have a complete line (contains newline or certain punctuation)
|
|
119
|
+
if (chunk.includes('\n') || chunk.includes('.') || chunk.includes('!') || chunk.includes('?') || chunk.includes(';')) {
|
|
120
|
+
fullResponse += currentLine;
|
|
121
|
+
currentLine = '';
|
|
122
|
+
setMessages(prev => {
|
|
123
|
+
const newMessages = [...prev];
|
|
124
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
125
|
+
if (lastMessage && lastMessage.streaming) {
|
|
126
|
+
lastMessage.content = fullResponse;
|
|
127
|
+
}
|
|
128
|
+
return newMessages;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Add any remaining content
|
|
133
|
+
if (currentLine && !controller.signal.aborted) {
|
|
134
|
+
fullResponse += currentLine;
|
|
135
|
+
setMessages(prev => {
|
|
136
|
+
const newMessages = [...prev];
|
|
137
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
138
|
+
if (lastMessage && lastMessage.streaming) {
|
|
139
|
+
lastMessage.content = fullResponse;
|
|
140
|
+
}
|
|
141
|
+
return newMessages;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
setMessages(prev => {
|
|
145
|
+
const newMessages = [...prev];
|
|
146
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
147
|
+
if (lastMessage && !lastMessage.discontinued) {
|
|
148
|
+
lastMessage.streaming = false;
|
|
149
|
+
}
|
|
150
|
+
return newMessages;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
156
|
+
setMessages(prev => {
|
|
157
|
+
const newMessages = [...prev];
|
|
158
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
159
|
+
if (lastMessage) {
|
|
160
|
+
lastMessage.content = `Error: ${errorMessage}`;
|
|
161
|
+
lastMessage.streaming = false;
|
|
162
|
+
}
|
|
163
|
+
return newMessages;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
setIsStreaming(false);
|
|
168
|
+
setAbortController(null);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const processPendingMessages = async () => {
|
|
172
|
+
if (pendingMessages.length === 0)
|
|
173
|
+
return;
|
|
174
|
+
// Get current pending messages and clear them immediately to prevent infinite loop
|
|
175
|
+
const messagesToProcess = [...pendingMessages];
|
|
176
|
+
setPendingMessages([]);
|
|
177
|
+
// Combine multiple pending messages into one
|
|
178
|
+
const combinedMessage = messagesToProcess.join('\n\n');
|
|
179
|
+
// Add user message to chat
|
|
180
|
+
const userMessage = { role: 'user', content: combinedMessage };
|
|
181
|
+
setMessages(prev => [...prev, userMessage]);
|
|
182
|
+
// Start streaming response (without calling processMessage to avoid recursion)
|
|
183
|
+
setIsStreaming(true);
|
|
184
|
+
// Create new abort controller for this request
|
|
185
|
+
const controller = new AbortController();
|
|
186
|
+
setAbortController(controller);
|
|
187
|
+
const assistantMessage = { role: 'assistant', content: '', streaming: true };
|
|
188
|
+
setMessages(prev => [...prev, assistantMessage]);
|
|
189
|
+
try {
|
|
190
|
+
const config = getOpenAiConfig();
|
|
191
|
+
const model = config.advancedModel || 'gpt-4.1';
|
|
192
|
+
// Check if request method is responses (not yet implemented)
|
|
193
|
+
if (config.requestMethod === 'responses') {
|
|
194
|
+
setMessages(prev => {
|
|
195
|
+
const newMessages = [...prev];
|
|
196
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
197
|
+
if (lastMessage) {
|
|
198
|
+
lastMessage.content = 'Responses API is not yet implemented. Please use "Chat Completions" method in API settings.';
|
|
199
|
+
lastMessage.streaming = false;
|
|
200
|
+
}
|
|
201
|
+
return newMessages;
|
|
202
|
+
});
|
|
203
|
+
// Don't return here, let it fall through to finally block
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
const chatMessages = [
|
|
207
|
+
{ role: 'system', content: 'You are a helpful coding assistant.' },
|
|
208
|
+
...messages.filter(msg => msg.role !== 'command').map(msg => ({ role: msg.role, content: msg.content })),
|
|
209
|
+
{ role: 'user', content: combinedMessage }
|
|
210
|
+
];
|
|
211
|
+
let fullResponse = '';
|
|
212
|
+
let currentLine = '';
|
|
213
|
+
for await (const chunk of createStreamingChatCompletion({
|
|
214
|
+
model,
|
|
215
|
+
messages: chatMessages,
|
|
216
|
+
temperature: 0
|
|
217
|
+
}, controller.signal)) {
|
|
218
|
+
if (controller.signal.aborted)
|
|
219
|
+
break;
|
|
220
|
+
currentLine += chunk;
|
|
221
|
+
// Check if we have a complete line (contains newline or certain punctuation)
|
|
222
|
+
if (chunk.includes('\n') || chunk.includes('.') || chunk.includes('!') || chunk.includes('?') || chunk.includes(';')) {
|
|
223
|
+
fullResponse += currentLine;
|
|
224
|
+
currentLine = '';
|
|
225
|
+
setMessages(prev => {
|
|
226
|
+
const newMessages = [...prev];
|
|
227
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
228
|
+
if (lastMessage && lastMessage.streaming) {
|
|
229
|
+
lastMessage.content = fullResponse;
|
|
230
|
+
}
|
|
231
|
+
return newMessages;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Add any remaining content
|
|
236
|
+
if (currentLine && !controller.signal.aborted) {
|
|
237
|
+
fullResponse += currentLine;
|
|
238
|
+
setMessages(prev => {
|
|
239
|
+
const newMessages = [...prev];
|
|
240
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
241
|
+
if (lastMessage && lastMessage.streaming) {
|
|
242
|
+
lastMessage.content = fullResponse;
|
|
243
|
+
}
|
|
244
|
+
return newMessages;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
setMessages(prev => {
|
|
248
|
+
const newMessages = [...prev];
|
|
249
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
250
|
+
if (lastMessage && !lastMessage.discontinued) {
|
|
251
|
+
lastMessage.streaming = false;
|
|
252
|
+
}
|
|
253
|
+
return newMessages;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
259
|
+
setMessages(prev => {
|
|
260
|
+
const newMessages = [...prev];
|
|
261
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
262
|
+
if (lastMessage) {
|
|
263
|
+
lastMessage.content = `Error: ${errorMessage}`;
|
|
264
|
+
lastMessage.streaming = false;
|
|
265
|
+
}
|
|
266
|
+
return newMessages;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
setIsStreaming(false);
|
|
271
|
+
setAbortController(null);
|
|
272
|
+
// Note: No recursive call here, useEffect will handle next batch
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
276
|
+
React.createElement(Box, { marginBottom: 1, borderColor: 'cyan', borderStyle: "round", paddingX: 2, paddingY: 1 },
|
|
277
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
278
|
+
React.createElement(Text, { color: "white", bold: true },
|
|
279
|
+
React.createElement(Text, { color: "cyan" }, "\u2746 "),
|
|
280
|
+
React.createElement(Gradient, { name: "rainbow" }, "Programming efficiency x10!")),
|
|
281
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "\u2022 Ask for code explanations and debugging help"),
|
|
282
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "\u2022 Press ESC during response to interrupt"))),
|
|
283
|
+
React.createElement(MessageList, { messages: messages, animationFrame: animationFrame, maxMessages: 6 }),
|
|
284
|
+
React.createElement(PendingMessages, { pendingMessages: pendingMessages }),
|
|
285
|
+
React.createElement(Box, { marginBottom: 0 },
|
|
286
|
+
React.createElement(ChatInput, { onSubmit: handleMessageSubmit, onCommand: handleCommandExecution, placeholder: "Ask me anything about coding...", disabled: false }))));
|
|
287
|
+
}
|