openaxies 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -3
- package/src/App.js +336 -234
- package/src/components/ModelBar.js +42 -0
- package/src/components/Particles.js +47 -0
- package/src/config/models.js +11 -5
- package/src/providers/index.js +16 -12
- package/src/providers/streaming.js +92 -96
- package/src/components/BrandHeader.js +0 -60
- package/src/components/ChatViewport.js +0 -193
- package/src/components/RouterBar.js +0 -98
package/src/App.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Box, useInput, useStdout } from 'ink';
|
|
2
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
3
3
|
import { hex } from './config/theme.js';
|
|
4
4
|
import { getCommands } from './config/commands.js';
|
|
5
|
-
import { getModelById, DEFAULT_MODEL_ID } from './config/models.js';
|
|
5
|
+
import { getModels, getModelById, DEFAULT_MODEL_ID } from './config/models.js';
|
|
6
6
|
import { callModel } from './providers/index.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { createChatViewport } from './components/ChatViewport.js';
|
|
10
|
-
import { createSlashOverlay, SLASH_OVERLAY_HEIGHT } from './components/SlashOverlay.js';
|
|
7
|
+
import { createModelBar } from './components/ModelBar.js';
|
|
8
|
+
import { createParticleRow } from './components/Particles.js';
|
|
11
9
|
import { createComposerDock, DOCK_HEIGHT } from './components/ComposerDock.js';
|
|
12
10
|
|
|
13
11
|
const h = React.createElement;
|
|
12
|
+
const BAR_COLOR = '#1A1A2E';
|
|
14
13
|
|
|
15
14
|
export default AppRoot;
|
|
16
15
|
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Helpers
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
16
|
function buildHistory(messages, newText) {
|
|
22
17
|
const history = [];
|
|
23
18
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -36,49 +31,74 @@ function buildHistory(messages, newText) {
|
|
|
36
31
|
return history;
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
function formatTimer(seconds) {
|
|
35
|
+
if (typeof seconds !== 'number' || seconds < 0) {
|
|
36
|
+
return '0.0s';
|
|
37
|
+
}
|
|
38
|
+
return seconds.toFixed(1) + 's';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function barLine(cols) {
|
|
42
|
+
const safeCols = typeof cols === 'number' && cols > 0 ? cols : 80;
|
|
43
|
+
let line = '';
|
|
44
|
+
for (let i = 0; i < safeCols; i++) {
|
|
45
|
+
line = line + '\u2500';
|
|
46
|
+
}
|
|
47
|
+
return h(Text, { color: BAR_COLOR }, line);
|
|
48
|
+
}
|
|
42
49
|
|
|
43
50
|
function AppRoot() {
|
|
44
51
|
const { stdout } = useStdout();
|
|
45
52
|
const rows = stdout.rows || 24;
|
|
46
53
|
const cols = stdout.columns || 80;
|
|
47
54
|
|
|
48
|
-
// --- UI state ---
|
|
49
|
-
const [inputBuffer, setInputBuffer] = React.useState('');
|
|
50
|
-
const [showOverlay, setShowOverlay] = React.useState(false);
|
|
51
|
-
const [overlayIndex, setOverlayIndex] = React.useState(0); // cursor inside overlay
|
|
52
|
-
|
|
53
|
-
// --- Chat state ---
|
|
54
55
|
const [messages, setMessages] = React.useState([]);
|
|
55
56
|
const [activeModel, setActiveModel] = React.useState(DEFAULT_MODEL_ID);
|
|
56
57
|
const [streamText, setStreamText] = React.useState('');
|
|
57
58
|
const [streamingActive, setStreamingActive] = React.useState(false);
|
|
58
|
-
const [scrollOffset, setScrollOffset] = React.useState(0);
|
|
59
59
|
const [isThinking, setIsThinking] = React.useState(false);
|
|
60
60
|
const [spinnerChar, setSpinnerChar] = React.useState('');
|
|
61
|
+
const [thinkingElapsed, setThinkingElapsed] = React.useState(0);
|
|
62
|
+
const [inputBuffer, setInputBuffer] = React.useState('');
|
|
63
|
+
const [showOverlay, setShowOverlay] = React.useState(false);
|
|
64
|
+
const [overlayIndex, setOverlayIndex] = React.useState(0);
|
|
65
|
+
const [particleFrame, setParticleFrame] = React.useState(0);
|
|
66
|
+
const [toolInfo, setToolInfo] = React.useState(null);
|
|
61
67
|
|
|
62
68
|
const commands = getCommands();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// SlashOverlay : SLASH_OVERLAY_HEIGHT (6) — conditional
|
|
69
|
-
// ComposerDock : DOCK_HEIGHT (3)
|
|
70
|
-
// ChatViewport : rows - all fixed rows (flexGrow fills remainder)
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
const overlayH = showOverlay ? SLASH_OVERLAY_HEIGHT : 0;
|
|
73
|
-
const fixedH = BRAND_HEIGHT + ROUTER_HEIGHT + overlayH + DOCK_HEIGHT;
|
|
69
|
+
const MODEL_BAR_H = 1;
|
|
70
|
+
const SEP_H = 1;
|
|
71
|
+
const DOCK = DOCK_HEIGHT;
|
|
72
|
+
const overlayH = showOverlay ? 6 : 0;
|
|
73
|
+
const fixedH = MODEL_BAR_H + SEP_H + overlayH + DOCK;
|
|
74
74
|
const availLines = Math.max(1, rows - fixedH);
|
|
75
75
|
|
|
76
76
|
const abortRef = React.useRef(null);
|
|
77
|
-
const
|
|
77
|
+
const connRef = React.useRef(null);
|
|
78
78
|
const spinnerRef = React.useRef(null);
|
|
79
|
-
const
|
|
79
|
+
const timerRef = React.useRef(null);
|
|
80
|
+
const thinkStartRef = React.useRef(null);
|
|
81
|
+
const particleRef = React.useRef(null);
|
|
82
|
+
|
|
83
|
+
const SPINNER_FRAMES = [
|
|
84
|
+
'\u25CB', '\u25D4', '\u25D0', '\u25D5', '\u25CF',
|
|
85
|
+
'\u25D5', '\u25D0', '\u25D4', '\u25CB', '\u25D1',
|
|
86
|
+
'\u25D2', '\u25D3', '\u25CF', '\u25D3', '\u25D2',
|
|
87
|
+
'\u25D1', '\u25CC', '\u25CB', '\u25CD', '\u25CE',
|
|
88
|
+
'\u25CF', '\u25CE', '\u25CD', '\u25CB',
|
|
89
|
+
];
|
|
80
90
|
|
|
81
|
-
|
|
91
|
+
React.useEffect(function () {
|
|
92
|
+
particleRef.current = setInterval(function () {
|
|
93
|
+
setParticleFrame(function (p) { return (p + 1) % 64; });
|
|
94
|
+
}, 100);
|
|
95
|
+
return function () {
|
|
96
|
+
if (particleRef.current !== null) {
|
|
97
|
+
clearInterval(particleRef.current);
|
|
98
|
+
particleRef.current = null;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}, []);
|
|
82
102
|
|
|
83
103
|
React.useEffect(function () {
|
|
84
104
|
if (isThinking === false) {
|
|
@@ -86,6 +106,10 @@ function AppRoot() {
|
|
|
86
106
|
clearInterval(spinnerRef.current);
|
|
87
107
|
spinnerRef.current = null;
|
|
88
108
|
}
|
|
109
|
+
if (timerRef.current !== null) {
|
|
110
|
+
clearInterval(timerRef.current);
|
|
111
|
+
timerRef.current = null;
|
|
112
|
+
}
|
|
89
113
|
setSpinnerChar('');
|
|
90
114
|
return;
|
|
91
115
|
}
|
|
@@ -94,128 +118,82 @@ function AppRoot() {
|
|
|
94
118
|
spinnerRef.current = setInterval(function () {
|
|
95
119
|
idx = (idx + 1) % SPINNER_FRAMES.length;
|
|
96
120
|
setSpinnerChar(SPINNER_FRAMES[idx]);
|
|
97
|
-
},
|
|
98
|
-
|
|
121
|
+
}, 100);
|
|
122
|
+
timerRef.current = setInterval(function () {
|
|
123
|
+
if (thinkStartRef.current !== null) {
|
|
124
|
+
setThinkingElapsed((Date.now() - thinkStartRef.current) / 1000);
|
|
125
|
+
}
|
|
126
|
+
}, 100);
|
|
127
|
+
return function () {
|
|
99
128
|
if (spinnerRef.current !== null) {
|
|
100
129
|
clearInterval(spinnerRef.current);
|
|
101
130
|
spinnerRef.current = null;
|
|
102
131
|
}
|
|
132
|
+
if (timerRef.current !== null) {
|
|
133
|
+
clearInterval(timerRef.current);
|
|
134
|
+
timerRef.current = null;
|
|
135
|
+
}
|
|
103
136
|
};
|
|
104
137
|
}, [isThinking]);
|
|
105
138
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
setOverlayIndex(0);
|
|
117
|
-
}
|
|
118
|
-
// Close overlay if buffer no longer starts with '/'
|
|
119
|
-
if (showOverlay === true && (safeValue.length === 0 || safeValue[0] !== '/')) {
|
|
120
|
-
setShowOverlay(false);
|
|
121
|
-
}
|
|
139
|
+
function cycleModel() {
|
|
140
|
+
const models = getModels();
|
|
141
|
+
setActiveModel(function (prev) {
|
|
142
|
+
let found = false;
|
|
143
|
+
for (let i = 0; i < models.length; i++) {
|
|
144
|
+
if (found) return models[i].id;
|
|
145
|
+
if (models[i].id === prev) found = true;
|
|
146
|
+
}
|
|
147
|
+
return models[0].id;
|
|
148
|
+
});
|
|
122
149
|
}
|
|
123
150
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
function handleSlashSelect(item) {
|
|
130
|
-
if (item === null || item === undefined) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const trigger = item.value;
|
|
134
|
-
if (typeof trigger !== 'string') {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (trigger === '/exit') {
|
|
139
|
-
process.exit(0);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (trigger === '/clear') {
|
|
143
|
-
setMessages([]);
|
|
144
|
-
setScrollOffset(0);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (trigger === '/model') {
|
|
148
|
-
// Toggle between the two local models
|
|
149
|
-
setActiveModel(function (prev) {
|
|
150
|
-
if (prev === 'openaxis/openaxis-flash') {
|
|
151
|
-
return 'openaxis/openaxis-heavy';
|
|
152
|
-
}
|
|
153
|
-
return 'openaxis/openaxis-flash';
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (trigger === '/help') {
|
|
158
|
-
setScrollOffset(0);
|
|
159
|
-
setMessages(function (prev) {
|
|
160
|
-
return prev.concat([{
|
|
161
|
-
role: 'assistant',
|
|
162
|
-
content:
|
|
163
|
-
'**Available commands:**\n' +
|
|
164
|
-
'- `/help` — Show this help\n' +
|
|
165
|
-
'- `/clear` — Clear the conversation\n' +
|
|
166
|
-
'- `/model` — Toggle between Flash and Heavy\n' +
|
|
167
|
-
'- `/exit` — Quit the application\n\n' +
|
|
168
|
-
'Type your message and press **Enter** to chat.',
|
|
169
|
-
id: 'help-' + Date.now(),
|
|
170
|
-
}]);
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Always close & clear after any selection
|
|
175
|
-
setInputBuffer('');
|
|
176
|
-
setShowOverlay(false);
|
|
177
|
-
setOverlayIndex(0);
|
|
151
|
+
function checkThinkState(text) {
|
|
152
|
+
const openIdx = text.lastIndexOf('<think>');
|
|
153
|
+
const closeIdx = text.lastIndexOf('</think>');
|
|
154
|
+
if (openIdx === -1 && closeIdx === -1) return false;
|
|
155
|
+
return openIdx > closeIdx;
|
|
178
156
|
}
|
|
179
157
|
|
|
180
158
|
async function handleSubmit(text) {
|
|
181
159
|
const safeText = typeof text === 'string' ? text : '';
|
|
182
|
-
if (safeText.length === 0 || streamingActive === true)
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const msgId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
|
160
|
+
if (safeText.length === 0 || streamingActive === true) return;
|
|
187
161
|
|
|
188
162
|
setStreamingActive(true);
|
|
189
|
-
|
|
190
|
-
setMessages(function (prev) {
|
|
191
|
-
return prev.concat([{ role: 'user', content: safeText, id: msgId }]);
|
|
192
|
-
});
|
|
193
|
-
setInputBuffer('');
|
|
163
|
+
setToolInfo(null);
|
|
194
164
|
setStreamText(' connecting');
|
|
195
|
-
|
|
165
|
+
setThinkingElapsed(0);
|
|
166
|
+
connRef.current = setInterval(function () {
|
|
196
167
|
setStreamText(function (prev) {
|
|
197
168
|
if (prev === ' connecting') return ' connecting.';
|
|
198
169
|
if (prev === ' connecting.') return ' connecting..';
|
|
199
170
|
if (prev === ' connecting..') return ' connecting...';
|
|
200
|
-
|
|
201
|
-
return prev;
|
|
171
|
+
return ' connecting';
|
|
202
172
|
});
|
|
203
173
|
}, 400);
|
|
204
174
|
|
|
175
|
+
setMessages(function (prev) {
|
|
176
|
+
return prev.concat([{
|
|
177
|
+
role: 'user',
|
|
178
|
+
content: safeText,
|
|
179
|
+
id: 'u-' + Date.now(),
|
|
180
|
+
}]);
|
|
181
|
+
});
|
|
182
|
+
setInputBuffer('');
|
|
183
|
+
|
|
205
184
|
const modelInfo = getModelById(activeModel);
|
|
206
|
-
if (modelInfo === null
|
|
207
|
-
if (
|
|
208
|
-
clearInterval(
|
|
209
|
-
|
|
185
|
+
if (modelInfo === null) {
|
|
186
|
+
if (connRef.current !== null) {
|
|
187
|
+
clearInterval(connRef.current);
|
|
188
|
+
connRef.current = null;
|
|
210
189
|
}
|
|
211
190
|
setStreamingActive(false);
|
|
212
191
|
setStreamText('');
|
|
213
|
-
setScrollOffset(0);
|
|
214
192
|
setMessages(function (prev) {
|
|
215
193
|
return prev.concat([{
|
|
216
194
|
role: 'assistant',
|
|
217
195
|
content: 'No model selected. Use `/model` to switch.',
|
|
218
|
-
id: '
|
|
196
|
+
id: 'e-' + Date.now(),
|
|
219
197
|
}]);
|
|
220
198
|
});
|
|
221
199
|
return;
|
|
@@ -226,115 +204,151 @@ function AppRoot() {
|
|
|
226
204
|
abortRef.current = abortController;
|
|
227
205
|
|
|
228
206
|
try {
|
|
229
|
-
function checkThinkState(text) {
|
|
230
|
-
const openIdx = text.lastIndexOf('<think>');
|
|
231
|
-
const closeIdx = text.lastIndexOf('</think>');
|
|
232
|
-
if (openIdx === -1 && closeIdx === -1) {
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
if (openIdx > closeIdx) {
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
207
|
const gen = callModel(modelInfo, history, abortController.signal);
|
|
242
208
|
let accumulated = '';
|
|
243
209
|
let firstEvent = true;
|
|
210
|
+
let thinkStarted = false;
|
|
244
211
|
|
|
245
212
|
for await (const event of gen) {
|
|
246
|
-
if (abortController.signal.aborted === true)
|
|
247
|
-
|
|
248
|
-
}
|
|
213
|
+
if (abortController.signal.aborted === true) break;
|
|
214
|
+
|
|
249
215
|
if (firstEvent === true) {
|
|
250
216
|
firstEvent = false;
|
|
251
|
-
if (
|
|
252
|
-
clearInterval(
|
|
253
|
-
|
|
217
|
+
if (connRef.current !== null) {
|
|
218
|
+
clearInterval(connRef.current);
|
|
219
|
+
connRef.current = null;
|
|
254
220
|
}
|
|
255
221
|
}
|
|
222
|
+
|
|
223
|
+
if (event.type === 'tool') {
|
|
224
|
+
setToolInfo({
|
|
225
|
+
tool: event.tool,
|
|
226
|
+
used: event.used,
|
|
227
|
+
sites: event.sites,
|
|
228
|
+
query: event.query,
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
256
233
|
if (event.type === 'thinking' || event.type === 'token') {
|
|
257
234
|
const content = typeof event.content === 'string' ? event.content : '';
|
|
258
235
|
for (let i = 0; i < content.length; i++) {
|
|
259
236
|
accumulated = accumulated + content[i];
|
|
260
|
-
const
|
|
261
|
-
setScrollOffset(0);
|
|
237
|
+
const insideThink = checkThinkState(accumulated);
|
|
262
238
|
setStreamText(accumulated);
|
|
263
|
-
setIsThinking(
|
|
264
|
-
if (
|
|
265
|
-
|
|
239
|
+
setIsThinking(insideThink);
|
|
240
|
+
if (insideThink === true && thinkStarted === false) {
|
|
241
|
+
thinkStarted = true;
|
|
242
|
+
thinkStartRef.current = Date.now();
|
|
243
|
+
setThinkingElapsed(0);
|
|
244
|
+
}
|
|
245
|
+
if (insideThink === false && thinkStarted === true) {
|
|
246
|
+
thinkStarted = false;
|
|
247
|
+
thinkStartRef.current = null;
|
|
248
|
+
setThinkingElapsed(0);
|
|
266
249
|
}
|
|
267
|
-
await new Promise(function (
|
|
268
|
-
setTimeout(resolve, 0);
|
|
269
|
-
});
|
|
250
|
+
await new Promise(function (r) { setTimeout(r, 0); });
|
|
270
251
|
}
|
|
271
252
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
253
|
+
|
|
254
|
+
if (event.type === 'done') break;
|
|
275
255
|
if (event.type === 'timeout') {
|
|
276
256
|
if (accumulated.length > 0) {
|
|
277
|
-
accumulated = accumulated + '\n\n
|
|
257
|
+
accumulated = accumulated + '\n\n\u2014 stream timed out \u2014';
|
|
278
258
|
}
|
|
279
259
|
setStreamText(accumulated);
|
|
280
260
|
break;
|
|
281
261
|
}
|
|
282
262
|
}
|
|
283
263
|
|
|
284
|
-
if (
|
|
285
|
-
clearInterval(
|
|
286
|
-
|
|
264
|
+
if (connRef.current !== null) {
|
|
265
|
+
clearInterval(connRef.current);
|
|
266
|
+
connRef.current = null;
|
|
287
267
|
}
|
|
288
268
|
setIsThinking(false);
|
|
289
|
-
|
|
269
|
+
thinkStartRef.current = null;
|
|
270
|
+
setThinkingElapsed(0);
|
|
290
271
|
setStreamingActive(false);
|
|
272
|
+
|
|
273
|
+
const finalText = streamText;
|
|
291
274
|
setStreamText('');
|
|
292
275
|
|
|
293
|
-
if (
|
|
276
|
+
if (finalText.length > 0) {
|
|
294
277
|
setMessages(function (prev) {
|
|
295
278
|
return prev.concat([{
|
|
296
279
|
role: 'assistant',
|
|
297
|
-
content:
|
|
298
|
-
id: '
|
|
280
|
+
content: finalText,
|
|
281
|
+
id: 'a-' + Date.now(),
|
|
299
282
|
}]);
|
|
300
283
|
});
|
|
301
284
|
}
|
|
302
285
|
} catch (err) {
|
|
303
|
-
if (
|
|
304
|
-
clearInterval(
|
|
305
|
-
|
|
286
|
+
if (connRef.current !== null) {
|
|
287
|
+
clearInterval(connRef.current);
|
|
288
|
+
connRef.current = null;
|
|
306
289
|
}
|
|
307
290
|
setIsThinking(false);
|
|
308
|
-
|
|
291
|
+
thinkStartRef.current = null;
|
|
292
|
+
setThinkingElapsed(0);
|
|
309
293
|
setStreamingActive(false);
|
|
310
294
|
setStreamText('');
|
|
311
|
-
|
|
312
|
-
const errMsg = (err !== null && err !== undefined)
|
|
295
|
+
const errMsg = err !== null && err !== undefined
|
|
313
296
|
? (err.message || String(err))
|
|
314
297
|
: 'Unknown error';
|
|
298
|
+
setMessages(function (prev) {
|
|
299
|
+
return prev.concat([{
|
|
300
|
+
role: 'assistant',
|
|
301
|
+
content: '**Error:** ' + errMsg + '\n\nThe Space may be cold-starting (30\u2013120 s first request). Try again.',
|
|
302
|
+
id: 'er-' + Date.now(),
|
|
303
|
+
}]);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function handleInputChange(v) {
|
|
309
|
+
const safe = typeof v === 'string' ? v : '';
|
|
310
|
+
setInputBuffer(safe);
|
|
311
|
+
if (safe.length === 1 && safe[0] === '/' && showOverlay === false) {
|
|
312
|
+
setShowOverlay(true);
|
|
313
|
+
setOverlayIndex(0);
|
|
314
|
+
}
|
|
315
|
+
if (showOverlay === true && (safe.length === 0 || safe[0] !== '/')) {
|
|
316
|
+
setShowOverlay(false);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function handleSlashSelect(item) {
|
|
321
|
+
if (item === null || item === undefined) return;
|
|
322
|
+
const trigger = item.value;
|
|
323
|
+
if (typeof trigger !== 'string') return;
|
|
324
|
+
|
|
325
|
+
if (trigger === '/exit') process.exit(0);
|
|
326
|
+
if (trigger === '/clear') {
|
|
327
|
+
setMessages([]);
|
|
328
|
+
}
|
|
329
|
+
if (trigger === '/model') {
|
|
330
|
+
cycleModel();
|
|
331
|
+
}
|
|
332
|
+
if (trigger === '/help') {
|
|
315
333
|
setMessages(function (prev) {
|
|
316
334
|
return prev.concat([{
|
|
317
335
|
role: 'assistant',
|
|
318
336
|
content:
|
|
319
|
-
'**
|
|
320
|
-
'
|
|
321
|
-
|
|
337
|
+
'**Commands:**\n' +
|
|
338
|
+
'- `/help` \u2014 this help\n' +
|
|
339
|
+
'- `/clear` \u2014 clear chat\n' +
|
|
340
|
+
'- `/model` \u2014 cycle: Llama \u2192 GPT \u2192 DeepSeek\n' +
|
|
341
|
+
'- `/exit` \u2014 quit',
|
|
342
|
+
id: 'hlp-' + Date.now(),
|
|
322
343
|
}]);
|
|
323
344
|
});
|
|
324
345
|
}
|
|
346
|
+
setInputBuffer('');
|
|
347
|
+
setShowOverlay(false);
|
|
348
|
+
setOverlayIndex(0);
|
|
325
349
|
}
|
|
326
350
|
|
|
327
|
-
// ---------------------------------------------------------------------------
|
|
328
|
-
// Keyboard router
|
|
329
|
-
//
|
|
330
|
-
// Overlay mode: ↑↓ move cursor · Enter selects · Esc dismisses
|
|
331
|
-
// All other keys are absorbed (TextInput has focus=false)
|
|
332
|
-
//
|
|
333
|
-
// Normal mode: Enter submits · Ctrl+C abort/quit
|
|
334
|
-
// ---------------------------------------------------------------------------
|
|
335
351
|
useInput(function handleInput(input, key) {
|
|
336
|
-
|
|
337
|
-
// ── Overlay branch ──────────────────────────────────────────────────────
|
|
338
352
|
if (showOverlay === true) {
|
|
339
353
|
if (key.escape === true) {
|
|
340
354
|
setShowOverlay(false);
|
|
@@ -342,63 +356,33 @@ function AppRoot() {
|
|
|
342
356
|
setOverlayIndex(0);
|
|
343
357
|
return;
|
|
344
358
|
}
|
|
345
|
-
|
|
346
359
|
if (key.upArrow === true) {
|
|
347
|
-
setOverlayIndex(function (
|
|
348
|
-
return prev > 0 ? prev - 1 : commands.length - 1; // wrap around top
|
|
349
|
-
});
|
|
360
|
+
setOverlayIndex(function (p) { return p > 0 ? p - 1 : commands.length - 1; });
|
|
350
361
|
return;
|
|
351
362
|
}
|
|
352
|
-
|
|
353
363
|
if (key.downArrow === true) {
|
|
354
|
-
setOverlayIndex(function (
|
|
355
|
-
return prev < commands.length - 1 ? prev + 1 : 0; // wrap around bottom
|
|
356
|
-
});
|
|
364
|
+
setOverlayIndex(function (p) { return p < commands.length - 1 ? p + 1 : 0; });
|
|
357
365
|
return;
|
|
358
366
|
}
|
|
359
|
-
|
|
360
367
|
if (key.return === true) {
|
|
361
|
-
const
|
|
362
|
-
if (
|
|
363
|
-
handleSlashSelect({ value: selectedCmd.trigger });
|
|
364
|
-
}
|
|
368
|
+
const cmd = commands[overlayIndex];
|
|
369
|
+
if (cmd !== null && cmd !== undefined) handleSlashSelect({ value: cmd.trigger });
|
|
365
370
|
return;
|
|
366
371
|
}
|
|
367
|
-
|
|
368
|
-
// Absorb everything else while overlay is open
|
|
369
372
|
return;
|
|
370
373
|
}
|
|
371
374
|
|
|
372
|
-
// ── Normal branch ────────────────────────────────────────────────────────
|
|
373
375
|
if (key.return === true) {
|
|
374
|
-
const
|
|
375
|
-
if (
|
|
376
|
-
handleSubmit(
|
|
376
|
+
const safe = typeof inputBuffer === 'string' ? inputBuffer : '';
|
|
377
|
+
if (safe.length > 0 && streamingActive === false) {
|
|
378
|
+
handleSubmit(safe);
|
|
377
379
|
}
|
|
378
380
|
return;
|
|
379
381
|
}
|
|
380
382
|
|
|
381
|
-
if (key.pageUp === true || (key.ctrl === true && key.upArrow === true)) {
|
|
382
|
-
const pageSize = Math.max(1, availLines - 2);
|
|
383
|
-
setScrollOffset(function (prev) {
|
|
384
|
-
return prev + pageSize;
|
|
385
|
-
});
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (key.pageDown === true || (key.ctrl === true && key.downArrow === true)) {
|
|
390
|
-
const pageSize = Math.max(1, availLines - 2);
|
|
391
|
-
setScrollOffset(function (prev) {
|
|
392
|
-
return Math.max(0, prev - pageSize);
|
|
393
|
-
});
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
383
|
if (key.ctrl === true && (input === 'c' || input === 'C')) {
|
|
398
384
|
if (abortRef.current !== null) {
|
|
399
|
-
try {
|
|
400
|
-
abortRef.current.abort();
|
|
401
|
-
} catch (_) { }
|
|
385
|
+
try { abortRef.current.abort(); } catch (_) {}
|
|
402
386
|
abortRef.current = null;
|
|
403
387
|
setStreamingActive(false);
|
|
404
388
|
setStreamText('');
|
|
@@ -407,27 +391,141 @@ function AppRoot() {
|
|
|
407
391
|
}
|
|
408
392
|
});
|
|
409
393
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
394
|
+
const modelBar = createModelBar(activeModel, cols);
|
|
395
|
+
const particleTop = createParticleRow(particleFrame, cols, 'tp');
|
|
396
|
+
|
|
397
|
+
const viewLines = [];
|
|
398
|
+
for (let i = 0; i < messages.length; i++) {
|
|
399
|
+
const msg = messages[i];
|
|
400
|
+
if (msg === null || typeof msg !== 'object') continue;
|
|
401
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
402
|
+
if (content.length === 0) continue;
|
|
403
|
+
|
|
404
|
+
if (i > 0) viewLines.push({ type: 'sep' });
|
|
405
|
+
|
|
406
|
+
if (msg.role === 'user') {
|
|
407
|
+
viewLines.push({ type: 'user', text: content });
|
|
408
|
+
viewLines.push({ type: 'sep' });
|
|
409
|
+
} else {
|
|
410
|
+
viewLines.push({ type: 'assistant', text: content });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const hasStream = typeof streamText === 'string' && streamText.length > 0;
|
|
415
|
+
if (hasStream === true) {
|
|
416
|
+
viewLines.push({ type: 'sep' });
|
|
417
|
+
|
|
418
|
+
if (toolInfo !== null && toolInfo.used === true) {
|
|
419
|
+
viewLines.push({
|
|
420
|
+
type: 'tool',
|
|
421
|
+
text: ' \u2500\u2500 ' + toolInfo.tool + ': "' + toolInfo.query + '" (' + toolInfo.sites + ' sites)',
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (isThinking === true) {
|
|
426
|
+
const sp = typeof spinnerChar === 'string' && spinnerChar.length > 0 ? spinnerChar : '\u25CB';
|
|
427
|
+
viewLines.push({
|
|
428
|
+
type: 'think',
|
|
429
|
+
spin: sp,
|
|
430
|
+
elapsed: formatTimer(thinkingElapsed),
|
|
431
|
+
});
|
|
432
|
+
const clean = streamText.split('<think>').join('').split('</think>').join('');
|
|
433
|
+
viewLines.push({ type: 'stream', text: clean, color: '#FF9F43' });
|
|
434
|
+
} else {
|
|
435
|
+
const clean = streamText.split('<think>').join('').split('</think>').join('');
|
|
436
|
+
viewLines.push({ type: 'stream', text: clean, color: hex.primary });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (viewLines.length === 0) {
|
|
441
|
+
viewLines.push({ type: 'muted', text: ' type a message to start...' });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const visibleHeight = Math.max(1, availLines - 1);
|
|
445
|
+
const displayLines = viewLines.slice(-visibleHeight);
|
|
446
|
+
const viewElements = [];
|
|
447
|
+
|
|
448
|
+
for (let i = 0; i < displayLines.length; i++) {
|
|
449
|
+
const line = displayLines[i];
|
|
450
|
+
if (line.type === 'sep') {
|
|
451
|
+
viewElements.push(h(Box, { key: 'vs-' + i, height: 1 }, barLine(cols)));
|
|
452
|
+
} else if (line.type === 'think') {
|
|
453
|
+
viewElements.push(
|
|
454
|
+
h(Box, { key: 'vt-' + i, height: 1 },
|
|
455
|
+
h(Text, { color: '#FF9F43', bold: true },
|
|
456
|
+
' ' + line.spin + ' Thinking \u2022 ' + line.elapsed
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
);
|
|
460
|
+
} else if (line.type === 'tool') {
|
|
461
|
+
viewElements.push(
|
|
462
|
+
h(Box, { key: 'vtl-' + i, height: 1 },
|
|
463
|
+
h(Text, { color: '#5B5B8A' }, line.text)
|
|
464
|
+
)
|
|
465
|
+
);
|
|
466
|
+
} else if (line.type === 'user') {
|
|
467
|
+
viewElements.push(
|
|
468
|
+
h(Box, {
|
|
469
|
+
key: 'vu-' + i,
|
|
470
|
+
height: 1,
|
|
471
|
+
paddingLeft: 1,
|
|
472
|
+
},
|
|
473
|
+
h(Text, { color: hex.neonBlue, bold: true }, '\u25B7 '),
|
|
474
|
+
h(Text, { color: hex.neonBlue }, line.text)
|
|
475
|
+
)
|
|
476
|
+
);
|
|
477
|
+
} else if (line.type === 'assistant') {
|
|
478
|
+
viewElements.push(
|
|
479
|
+
h(Box, {
|
|
480
|
+
key: 'va-' + i,
|
|
481
|
+
height: 1,
|
|
482
|
+
paddingLeft: 1,
|
|
483
|
+
},
|
|
484
|
+
h(Text, { color: hex.primary }, line.text)
|
|
485
|
+
)
|
|
486
|
+
);
|
|
487
|
+
} else if (line.type === 'stream') {
|
|
488
|
+
viewElements.push(
|
|
489
|
+
h(Box, {
|
|
490
|
+
key: 'vst-' + i,
|
|
491
|
+
height: 1,
|
|
492
|
+
paddingLeft: 1,
|
|
493
|
+
},
|
|
494
|
+
h(Text, { color: line.color }, line.text)
|
|
495
|
+
)
|
|
496
|
+
);
|
|
497
|
+
} else if (line.type === 'muted') {
|
|
498
|
+
viewElements.push(
|
|
499
|
+
h(Box, { key: 'vm-' + i, height: 1, paddingLeft: 1 },
|
|
500
|
+
h(Text, { color: '#333355' }, line.text)
|
|
501
|
+
)
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const viewport = h(Box, {
|
|
507
|
+
flexGrow: 1,
|
|
508
|
+
height: availLines,
|
|
509
|
+
flexDirection: 'column',
|
|
510
|
+
overflow: 'hidden',
|
|
511
|
+
paddingBottom: 1,
|
|
512
|
+
}, ...viewElements);
|
|
513
|
+
|
|
514
|
+
const particleBot = createParticleRow(particleFrame, cols, 'bt');
|
|
413
515
|
|
|
414
|
-
const header = createBrandHeader(cols);
|
|
415
|
-
const routerBar = createRouterBar(activeModel, cols);
|
|
416
|
-
const viewport = createChatViewport(messages, streamText, availLines, cols, scrollOffset, isThinking, spinnerChar);
|
|
417
516
|
const slashOverlay = showOverlay === true
|
|
418
517
|
? createSlashOverlay(commands, overlayIndex)
|
|
419
518
|
: null;
|
|
519
|
+
|
|
420
520
|
const dock = createComposerDock({
|
|
421
521
|
value: inputBuffer,
|
|
422
522
|
onChange: handleInputChange,
|
|
423
|
-
onSubmit:
|
|
424
|
-
inputActive: showOverlay === false,
|
|
523
|
+
onSubmit: function () {},
|
|
524
|
+
inputActive: showOverlay === false,
|
|
425
525
|
isStreaming: streamingActive,
|
|
426
526
|
terminalWidth: cols,
|
|
427
527
|
});
|
|
428
528
|
|
|
429
|
-
// Strict column layout — total height === rows (no scroll, no overflow).
|
|
430
|
-
// ComposerDock has flexShrink:0 so it is always pinned at bottom.
|
|
431
529
|
return h(Box, {
|
|
432
530
|
flexDirection: 'column',
|
|
433
531
|
width: '100%',
|
|
@@ -435,10 +533,14 @@ function AppRoot() {
|
|
|
435
533
|
backgroundColor: hex.black,
|
|
436
534
|
overflow: 'hidden',
|
|
437
535
|
},
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
536
|
+
modelBar,
|
|
537
|
+
h(Box, { height: 1 }, barLine(cols)),
|
|
538
|
+
h(Box, { height: 1, paddingLeft: 1 }, particleTop),
|
|
539
|
+
viewport,
|
|
540
|
+
h(Box, { height: 1, paddingLeft: 1 }, particleBot),
|
|
541
|
+
slashOverlay,
|
|
542
|
+
dock
|
|
443
543
|
);
|
|
444
544
|
}
|
|
545
|
+
|
|
546
|
+
import { createSlashOverlay } from './components/SlashOverlay.js';
|