openaxies 0.3.0 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openaxies",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/src/App.js CHANGED
@@ -56,6 +56,8 @@ function AppRoot() {
56
56
  const [streamText, setStreamText] = React.useState('');
57
57
  const [streamingActive, setStreamingActive] = React.useState(false);
58
58
  const [scrollOffset, setScrollOffset] = React.useState(0);
59
+ const [isThinking, setIsThinking] = React.useState(false);
60
+ const [spinnerChar, setSpinnerChar] = React.useState('');
59
61
 
60
62
  const commands = getCommands();
61
63
 
@@ -73,6 +75,33 @@ function AppRoot() {
73
75
 
74
76
  const abortRef = React.useRef(null);
75
77
  const connectingRef = React.useRef(null);
78
+ const spinnerRef = React.useRef(null);
79
+ const thinkingAccumRef = React.useRef('');
80
+
81
+ const SPINNER_FRAMES = ['◐', '◓', '◑', '◒'];
82
+
83
+ React.useEffect(function () {
84
+ if (isThinking === false) {
85
+ if (spinnerRef.current !== null) {
86
+ clearInterval(spinnerRef.current);
87
+ spinnerRef.current = null;
88
+ }
89
+ setSpinnerChar('');
90
+ return;
91
+ }
92
+ let idx = 0;
93
+ setSpinnerChar(SPINNER_FRAMES[0]);
94
+ spinnerRef.current = setInterval(function () {
95
+ idx = (idx + 1) % SPINNER_FRAMES.length;
96
+ setSpinnerChar(SPINNER_FRAMES[idx]);
97
+ }, 120);
98
+ return function cleanup() {
99
+ if (spinnerRef.current !== null) {
100
+ clearInterval(spinnerRef.current);
101
+ spinnerRef.current = null;
102
+ }
103
+ };
104
+ }, [isThinking]);
76
105
 
77
106
  // ---------------------------------------------------------------------------
78
107
  // Input handlers
@@ -118,10 +147,10 @@ function AppRoot() {
118
147
  if (trigger === '/model') {
119
148
  // Toggle between the two local models
120
149
  setActiveModel(function (prev) {
121
- if (prev === 'openaxis/openaxis-mixtral') {
150
+ if (prev === 'openaxis/openaxis-flash') {
122
151
  return 'openaxis/openaxis-heavy';
123
152
  }
124
- return 'openaxis/openaxis-mixtral';
153
+ return 'openaxis/openaxis-flash';
125
154
  });
126
155
  }
127
156
 
@@ -134,7 +163,7 @@ function AppRoot() {
134
163
  '**Available commands:**\n' +
135
164
  '- `/help` — Show this help\n' +
136
165
  '- `/clear` — Clear the conversation\n' +
137
- '- `/model` — Toggle between Mixtral and Heavy\n' +
166
+ '- `/model` — Toggle between Flash and Heavy\n' +
138
167
  '- `/exit` — Quit the application\n\n' +
139
168
  'Type your message and press **Enter** to chat.',
140
169
  id: 'help-' + Date.now(),
@@ -197,6 +226,18 @@ function AppRoot() {
197
226
  abortRef.current = abortController;
198
227
 
199
228
  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
+
200
241
  const gen = callModel(modelInfo, history, abortController.signal);
201
242
  let accumulated = '';
202
243
  let firstEvent = true;
@@ -216,8 +257,13 @@ function AppRoot() {
216
257
  const content = typeof event.content === 'string' ? event.content : '';
217
258
  for (let i = 0; i < content.length; i++) {
218
259
  accumulated = accumulated + content[i];
260
+ const isInsideThink = checkThinkState(accumulated);
219
261
  setScrollOffset(0);
220
262
  setStreamText(accumulated);
263
+ setIsThinking(isInsideThink);
264
+ if (isInsideThink === true) {
265
+ thinkingAccumRef.current = accumulated;
266
+ }
221
267
  await new Promise(function (resolve) {
222
268
  setTimeout(resolve, 0);
223
269
  });
@@ -239,6 +285,8 @@ function AppRoot() {
239
285
  clearInterval(connectingRef.current);
240
286
  connectingRef.current = null;
241
287
  }
288
+ setIsThinking(false);
289
+ thinkingAccumRef.current = '';
242
290
  setStreamingActive(false);
243
291
  setStreamText('');
244
292
 
@@ -256,6 +304,8 @@ function AppRoot() {
256
304
  clearInterval(connectingRef.current);
257
305
  connectingRef.current = null;
258
306
  }
307
+ setIsThinking(false);
308
+ thinkingAccumRef.current = '';
259
309
  setStreamingActive(false);
260
310
  setStreamText('');
261
311
  setScrollOffset(0);
@@ -363,7 +413,7 @@ function AppRoot() {
363
413
 
364
414
  const header = createBrandHeader(cols);
365
415
  const routerBar = createRouterBar(activeModel, cols);
366
- const viewport = createChatViewport(messages, streamText, availLines, cols, scrollOffset);
416
+ const viewport = createChatViewport(messages, streamText, availLines, cols, scrollOffset, isThinking, spinnerChar);
367
417
  const slashOverlay = showOverlay === true
368
418
  ? createSlashOverlay(commands, overlayIndex)
369
419
  : null;
@@ -4,6 +4,9 @@ import { hex } from '../config/theme.js';
4
4
 
5
5
  const h = React.createElement;
6
6
 
7
+ const THINK_OPEN = '<think>';
8
+ const THINK_CLOSE = '</think>';
9
+
7
10
  function checkArray(arr) {
8
11
  if (Array.isArray(arr) === false) {
9
12
  return [];
@@ -61,6 +64,39 @@ function pushWrapped(lines, text, width, color, bold) {
61
64
  }
62
65
  }
63
66
 
67
+ function stripThinkTags(text) {
68
+ if (typeof text !== 'string') {
69
+ return '';
70
+ }
71
+ return text.split(THINK_OPEN).join('').split(THINK_CLOSE).join('');
72
+ }
73
+
74
+ function extractThinkContent(text) {
75
+ if (typeof text !== 'string') {
76
+ return '';
77
+ }
78
+ const openIdx = text.indexOf(THINK_OPEN);
79
+ if (openIdx === -1) {
80
+ return '';
81
+ }
82
+ const closeIdx = text.indexOf(THINK_CLOSE, openIdx + THINK_OPEN.length);
83
+ if (closeIdx === -1) {
84
+ return text.slice(openIdx + THINK_OPEN.length);
85
+ }
86
+ return text.slice(openIdx + THINK_OPEN.length, closeIdx);
87
+ }
88
+
89
+ function extractAfterThink(text) {
90
+ if (typeof text !== 'string') {
91
+ return '';
92
+ }
93
+ const closeIdx = text.lastIndexOf(THINK_CLOSE);
94
+ if (closeIdx === -1) {
95
+ return '';
96
+ }
97
+ return text.slice(closeIdx + THINK_CLOSE.length);
98
+ }
99
+
64
100
  function pushRawMessage(lines, msg, safeCols) {
65
101
  if (msg === null || msg === undefined || typeof msg !== 'object') {
66
102
  return;
@@ -71,15 +107,27 @@ function pushRawMessage(lines, msg, safeCols) {
71
107
  return;
72
108
  }
73
109
 
110
+ const clean = stripThinkTags(content);
111
+ if (clean.length === 0) {
112
+ return;
113
+ }
114
+
74
115
  if (msg.role === 'user') {
75
- pushWrapped(lines, content, safeCols, hex.neonBlue, false);
116
+ pushWrapped(lines, clean, safeCols, hex.neonBlue, false);
76
117
  return;
77
118
  }
78
119
 
79
- pushWrapped(lines, content, safeCols, hex.primary, false);
120
+ pushWrapped(lines, clean, safeCols, hex.primary, false);
80
121
  }
81
122
 
82
- export function createChatViewport(messages, streamText, availLines, cols, scrollOffset) {
123
+ function pushSpinnerLine(lines, spinnerChar, safeCols) {
124
+ const safeChar = typeof spinnerChar === 'string' && spinnerChar.length > 0
125
+ ? spinnerChar
126
+ : '◐';
127
+ pushLine(lines, ' ' + safeChar + ' thinking', '#FF9F43', true);
128
+ }
129
+
130
+ export function createChatViewport(messages, streamText, availLines, cols, scrollOffset, isThinking, spinnerChar) {
83
131
  const safeMessages = checkArray(messages);
84
132
  const safeAvail = typeof availLines === 'number' && availLines > 0 ? availLines : 1;
85
133
  const safeCols = clampWidth(cols);
@@ -87,6 +135,7 @@ export function createChatViewport(messages, streamText, availLines, cols, scrol
87
135
  ? Math.floor(scrollOffset)
88
136
  : 0;
89
137
  const hasStream = typeof streamText === 'string' && streamText.length > 0;
138
+ const isStreamingThink = isThinking === true && hasStream === true;
90
139
  const transcriptLines = [];
91
140
 
92
141
  for (let i = 0; i < safeMessages.length; i++) {
@@ -94,7 +143,18 @@ export function createChatViewport(messages, streamText, availLines, cols, scrol
94
143
  }
95
144
 
96
145
  if (hasStream === true) {
97
- pushWrapped(transcriptLines, streamText, safeCols, hex.primary, false);
146
+ if (isStreamingThink === true) {
147
+ pushSpinnerLine(transcriptLines, spinnerChar, safeCols);
148
+ const thinkContent = extractThinkContent(streamText);
149
+ if (thinkContent.length > 0) {
150
+ pushWrapped(transcriptLines, thinkContent, safeCols, '#FF9F43', false);
151
+ }
152
+ } else {
153
+ const clean = stripThinkTags(streamText);
154
+ if (clean.length > 0) {
155
+ pushWrapped(transcriptLines, clean, safeCols, hex.primary, false);
156
+ }
157
+ }
98
158
  }
99
159
 
100
160
  if (transcriptLines.length === 0) {
@@ -8,7 +8,7 @@ export const ROUTER_HEIGHT = 2;
8
8
 
9
9
  function checkActiveModel(modelId) {
10
10
  if (typeof modelId !== 'string') {
11
- return 'openaxis/openaxis-mixtral';
11
+ return 'openaxis/openaxis-flash';
12
12
  }
13
13
  return modelId;
14
14
  }
@@ -34,7 +34,7 @@ function buildSeparator(cols) {
34
34
  // Row 2: full-width separator line
35
35
  export function createRouterBar(activeModel, cols) {
36
36
  const modelStr = checkActiveModel(activeModel);
37
- const isMixtral = modelStr === 'openaxis/openaxis-mixtral';
37
+ const isFlash = modelStr === 'openaxis/openaxis-flash';
38
38
  const sepLine = buildSeparator(cols);
39
39
 
40
40
  return h(Box, {
@@ -54,40 +54,40 @@ export function createRouterBar(activeModel, cols) {
54
54
 
55
55
  // ── Mixtral slot ──
56
56
  h(Text, {
57
- color: isMixtral ? hex.neonBlue : '#333355',
58
- bold: isMixtral,
59
- }, isMixtral ? '\u25B6 ' : '\u25B7 '),
57
+ color: isFlash ? hex.neonBlue : '#333355',
58
+ bold: isFlash,
59
+ }, isFlash ? '\u25B6 ' : '\u25B7 '),
60
60
  h(Text, {
61
- color: isMixtral ? hex.primary : '#444466',
62
- bold: isMixtral,
63
- }, 'Mixtral'),
61
+ color: isFlash ? hex.primary : '#444466',
62
+ bold: isFlash,
63
+ }, 'Flash'),
64
64
  h(Text, {
65
- color: isMixtral ? hex.greenOnline : '#2A3A2A',
65
+ color: isFlash ? hex.greenOnline : '#2A3A2A',
66
66
  }, ' \u25CF '),
67
67
  h(Text, {
68
- color: isMixtral ? hex.greenOnline : '#2A3A2A',
69
- dimColor: !isMixtral,
70
- }, isMixtral ? 'ACTIVE' : 'STANDBY'),
68
+ color: isFlash ? hex.greenOnline : '#2A3A2A',
69
+ dimColor: !isFlash,
70
+ }, isFlash ? 'ACTIVE' : 'STANDBY'),
71
71
 
72
72
  // ── Divider ──
73
73
  h(Text, { color: '#1E1E2E' }, ' \u2502 '),
74
74
 
75
75
  // ── Heavy slot ──
76
76
  h(Text, {
77
- color: !isMixtral ? hex.neonBlue : '#333355',
78
- bold: !isMixtral,
79
- }, !isMixtral ? '\u25B6 ' : '\u25B7 '),
77
+ color: !isFlash ? hex.neonBlue : '#333355',
78
+ bold: !isFlash,
79
+ }, !isFlash ? '\u25B6 ' : '\u25B7 '),
80
80
  h(Text, {
81
- color: !isMixtral ? hex.primary : '#444466',
82
- bold: !isMixtral,
81
+ color: !isFlash ? hex.primary : '#444466',
82
+ bold: !isFlash,
83
83
  }, 'Heavy'),
84
84
  h(Text, {
85
- color: !isMixtral ? hex.greenOnline : '#2A3A2A',
85
+ color: !isFlash ? hex.greenOnline : '#2A3A2A',
86
86
  }, ' \u25CF '),
87
87
  h(Text, {
88
- color: !isMixtral ? hex.greenOnline : '#2A3A2A',
89
- dimColor: isMixtral,
90
- }, !isMixtral ? 'ACTIVE' : 'STANDBY'),
88
+ color: !isFlash ? hex.greenOnline : '#2A3A2A',
89
+ dimColor: isFlash,
90
+ }, !isFlash ? 'ACTIVE' : 'STANDBY'),
91
91
 
92
92
  // ── Right-side hint ──
93
93
  h(Text, { color: '#1E1E2E' }, ' \u2502 '),
@@ -34,7 +34,7 @@ function checkCommands(arr) {
34
34
  const COMMAND_DEFS = Object.freeze([
35
35
  { trigger: '/help', description: 'Show available commands' },
36
36
  { trigger: '/clear', description: 'Clear the conversation' },
37
- { trigger: '/model', description: 'Switch between Mixtral and Heavy' },
37
+ { trigger: '/model', description: 'Switch between Flash and Heavy' },
38
38
  { trigger: '/exit', description: 'Quit the application' },
39
39
  ]);
40
40
 
@@ -33,12 +33,11 @@ function checkModels(arr) {
33
33
  return arr;
34
34
  }
35
35
 
36
- // Mixtral (fast/light) and Heavy (quality).
37
36
  const MODELS = Object.freeze([
38
37
  {
39
- id: 'openaxis/openaxis-mixtral',
38
+ id: 'openaxis/openaxis-flash',
40
39
  provider: 'openaxis',
41
- label: 'OpenAxies Mixtral',
40
+ label: 'OpenAxies Flash',
42
41
  badge: 'Fast',
43
42
  },
44
43
  {
@@ -5,7 +5,7 @@ import { runWebSearchGraph, shouldUseWebSearch } from './websearch.js';
5
5
  // HF Spaces cold-start on first request (30–120 s model download).
6
6
  // If the primary is waking up, we fall through to the next Space.
7
7
  const MODEL_ROUTES = Object.freeze({
8
- 'openaxis/openaxis-mixtral': Object.freeze([
8
+ 'openaxis/openaxis-flash': Object.freeze([
9
9
  'https://universal-618-clarity-main.hf.space/v1/chat/completions',
10
10
  'https://universal-618-clarity-2.hf.space/v1/chat/completions',
11
11
  'https://universal-618-clarity-3.hf.space/v1/chat/completions',