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.
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { hex } from '../config/theme.js';
4
+ import { getModels } from '../config/models.js';
5
+
6
+ const h = React.createElement;
7
+
8
+ const BAR_COLOR = '#1A1A2E';
9
+
10
+ export function createModelBar(activeId, cols) {
11
+ const models = getModels();
12
+ const safeCols = typeof cols === 'number' && cols > 0 ? cols : 80;
13
+ const parts = [];
14
+
15
+ for (let i = 0; i < models.length; i++) {
16
+ const m = models[i];
17
+ const isActive = m.id === activeId;
18
+ if (i > 0) {
19
+ parts.push(h(Text, { key: 'sp-' + i, color: BAR_COLOR }, ' \u2502 '));
20
+ }
21
+ const prefix = isActive ? '\u25B8 ' : ' ';
22
+ const dot = isActive ? '\u25CF' : '\u25CB';
23
+ const dotColor = isActive ? hex.greenOnline : '#333355';
24
+ const nameColor = isActive ? hex.neonBlue : '#555577';
25
+ const badgeColor = isActive ? hex.muted : '#333344';
26
+
27
+ parts.push(
28
+ h(Text, { key: 'pre-' + i, color: hex.primary }, prefix)
29
+ );
30
+ parts.push(
31
+ h(Text, { key: 'nm-' + i, color: nameColor, bold: isActive }, m.label)
32
+ );
33
+ parts.push(
34
+ h(Text, { key: 'bd-' + i, color: badgeColor }, ' [' + m.badge + ']')
35
+ );
36
+ parts.push(
37
+ h(Text, { key: 'dt-' + i, color: dotColor }, ' ' + dot)
38
+ );
39
+ }
40
+
41
+ return h(Box, { width: '100%', height: 1, paddingLeft: 1 }, ...parts);
42
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
+ import { hex } from '../config/theme.js';
4
+
5
+ const h = React.createElement;
6
+
7
+ const CHAR_SET = [
8
+ '\u2726', '\u00B7', '\u2727', '\u00B7', '\u2605', '\u00B7',
9
+ '\u2726', '\u00B7', '\u2219', '\u2727', '\u00B7', '\u2726',
10
+ '\u2219', '\u2605', '\u2219', '\u2727', '\u2219', '\u2726',
11
+ '\u00B7', '\u2727', '\u2605', '\u00B7', '\u2726', '\u00B7',
12
+ '\u2219', '\u2727',
13
+ ];
14
+
15
+ const COLORS = [
16
+ '#00F0FF', '#FF9F43', '#00FF88', '#FF6B9D', '#8B5CF6',
17
+ '#00F0FF', '#FFD700', '#00FF88',
18
+ ];
19
+
20
+ function getColor(frame, index, total) {
21
+ const phase = Math.floor((index / total) * 52);
22
+ const idx = (frame + phase) % COLORS.length;
23
+ return COLORS[idx];
24
+ }
25
+
26
+ export function createParticleRow(frame, cols, key) {
27
+ const safeCols = typeof cols === 'number' && cols > 0 ? cols : 80;
28
+ const safeFrame = typeof frame === 'number' ? frame : 0;
29
+ const parts = [];
30
+
31
+ for (let i = 0; i < 26; i++) {
32
+ const color = getColor(safeFrame, i, 52);
33
+ const ch = CHAR_SET[i % CHAR_SET.length];
34
+ parts.push(
35
+ h(Text, { key: key + '-' + i, color: color }, ch)
36
+ );
37
+ }
38
+
39
+ return h(Text, null, parts);
40
+ }
41
+
42
+ export function createParticlePair(frame, cols) {
43
+ return [
44
+ createParticleRow(frame, cols, 'top'),
45
+ createParticleRow(frame, cols, 'bot'),
46
+ ];
47
+ }
@@ -35,16 +35,22 @@ function checkModels(arr) {
35
35
 
36
36
  const MODELS = Object.freeze([
37
37
  {
38
- id: 'openaxis/openaxis-flash',
38
+ id: 'openaxis/openaxis-llama',
39
39
  provider: 'openaxis',
40
- label: 'OpenAxies Flash',
40
+ label: 'OpenAxies Llama',
41
41
  badge: 'Fast',
42
42
  },
43
43
  {
44
- id: 'openaxis/openaxis-heavy',
44
+ id: 'openaxis/openaxis-gpt',
45
45
  provider: 'openaxis',
46
- label: 'OpenAxies Heavy',
47
- badge: 'Quality',
46
+ label: 'OpenAxies GPT',
47
+ badge: 'Balanced',
48
+ },
49
+ {
50
+ id: 'openaxis/openaxis-deepseek',
51
+ provider: 'openaxis',
52
+ label: 'OpenAxies DeepSeek',
53
+ badge: 'Reasoning',
48
54
  },
49
55
  ]);
50
56
 
@@ -1,18 +1,17 @@
1
1
  import { streamResponse } from './streaming.js';
2
2
  import { runWebSearchGraph, shouldUseWebSearch } from './websearch.js';
3
3
 
4
- // Primary + two warm fallbacks per model.
5
- // HF Spaces cold-start on first request (30–120 s model download).
6
- // If the primary is waking up, we fall through to the next Space.
7
4
  const MODEL_ROUTES = Object.freeze({
8
- 'openaxis/openaxis-flash': Object.freeze([
5
+ 'openaxis/openaxis-llama': Object.freeze([
9
6
  'https://universal-618-clarity-main.hf.space/v1/chat/completions',
10
- 'https://universal-618-clarity-2.hf.space/v1/chat/completions',
11
- 'https://universal-618-clarity-3.hf.space/v1/chat/completions',
12
- ]),
13
- 'openaxis/openaxis-heavy': Object.freeze([
14
7
  'https://universal-618-clarity-4.hf.space/v1/chat/completions',
8
+ ]),
9
+ 'openaxis/openaxis-gpt': Object.freeze([
10
+ 'https://universal-618-clarity-2.hf.space/v1/chat/completions',
15
11
  'https://universal-618-clarity-5.hf.space/v1/chat/completions',
12
+ ]),
13
+ 'openaxis/openaxis-deepseek': Object.freeze([
14
+ 'https://universal-618-clarity-3.hf.space/v1/chat/completions',
16
15
  'https://universal-618-clarity-6.hf.space/v1/chat/completions',
17
16
  ]),
18
17
  });
@@ -53,11 +52,17 @@ export async function* callModel(modelConfig, messages, signal) {
53
52
 
54
53
  const modelShort = modelConfig.id.replace(/^[^/]+\//, '');
55
54
  let modelMessages = messages;
55
+ let webUsed = false;
56
+ let webSites = 0;
57
+ let webQuery = '';
56
58
 
57
59
  if (shouldUseWebSearch(messages) === true) {
58
60
  try {
59
61
  const webResult = await runWebSearchGraph(messages, signal);
60
62
  if (webResult.used === true && webResult.sites > 0) {
63
+ webUsed = true;
64
+ webSites = webResult.sites;
65
+ webQuery = webResult.query || '';
61
66
  modelMessages = [{
62
67
  role: 'system',
63
68
  content:
@@ -70,6 +75,8 @@ export async function* callModel(modelConfig, messages, signal) {
70
75
  }
71
76
  }
72
77
 
78
+ yield { type: 'tool', tool: 'websearch', used: webUsed, sites: webSites, query: webQuery };
79
+
73
80
  const body = {
74
81
  model: modelShort,
75
82
  messages: modelMessages,
@@ -79,7 +86,6 @@ export async function* callModel(modelConfig, messages, signal) {
79
86
 
80
87
  let lastError = null;
81
88
 
82
- // Try each endpoint in order — fall through if one throws or returns an error.
83
89
  for (let i = 0; i < endpoints.length; i++) {
84
90
  const endpoint = endpoints[i];
85
91
  try {
@@ -87,16 +93,14 @@ export async function* callModel(modelConfig, messages, signal) {
87
93
  for await (const event of stream) {
88
94
  yield event;
89
95
  }
90
- return; // clean exit — stream completed
96
+ return;
91
97
  } catch (err) {
92
98
  lastError = err;
93
- // Only retry on genuine failures, not on user-initiated abort
94
99
  if (signal !== null && signal !== undefined && signal.aborted === true) {
95
100
  return;
96
101
  }
97
102
  }
98
103
  }
99
104
 
100
- // All endpoints failed
101
105
  throw lastError !== null ? lastError : new Error('All endpoints failed for ' + modelConfig.id);
102
106
  }
@@ -1,20 +1,12 @@
1
- const CONNECT_TIMEOUT = 20000;
2
- const READ_TIMEOUT = 8000;
3
-
4
- function readWithTimeout(reader, timeoutMs) {
5
- return Promise.race([
6
- reader.read(),
7
- new Promise(function (_, reject) {
8
- setTimeout(function () {
9
- reject(new Error('stream_read_timeout'));
10
- }, timeoutMs);
11
- }),
12
- ]);
13
- }
1
+ const CONNECT_TIMEOUT = 120000;
2
+ const READ_TIMEOUT = 60000;
14
3
 
15
- function checkEndpoint(url) {
16
- if (typeof url !== 'string' || url.length === 0) {
17
- throw new Error('endpoint must be a non-empty URL string');
4
+ function checkEndpoint(endpoint) {
5
+ if (typeof endpoint !== 'string') {
6
+ throw new Error('endpoint must be a string');
7
+ }
8
+ if (endpoint.length === 0) {
9
+ throw new Error('endpoint must not be empty');
18
10
  }
19
11
  }
20
12
 
@@ -24,120 +16,124 @@ function checkBody(body) {
24
16
  }
25
17
  }
26
18
 
19
+ function checkSignal(signal) {
20
+ if (signal !== null && signal !== undefined && !(signal instanceof AbortSignal)) {
21
+ throw new Error('signal must be an AbortSignal or null');
22
+ }
23
+ }
24
+
27
25
  export async function* streamResponse(endpoint, body, signal) {
28
26
  checkEndpoint(endpoint);
29
27
  checkBody(body);
28
+ checkSignal(signal);
30
29
 
31
30
  const controller = new AbortController();
32
-
33
- function onAbort() {
34
- try {
35
- controller.abort();
36
- } catch (_) {
37
- }
38
- }
39
-
40
- if (signal !== null && signal !== undefined) {
41
- signal.addEventListener('abort', onAbort, { once: true });
42
- }
31
+ const combinedSignal = signal !== null && signal !== undefined
32
+ ? AbortSignal.any ? AbortSignal.any([signal, controller.signal]) : controller.signal
33
+ : controller.signal;
43
34
 
44
35
  const connectTimeout = setTimeout(function () {
45
- try {
46
- controller.abort();
47
- } catch (_) {
48
- }
36
+ controller.abort(new Error('Connect timeout: ' + CONNECT_TIMEOUT + 'ms'));
49
37
  }, CONNECT_TIMEOUT);
50
38
 
39
+ let response;
40
+
51
41
  try {
52
- const res = await fetch(endpoint, {
42
+ response = await fetch(endpoint, {
53
43
  method: 'POST',
54
- headers: { 'Content-Type': 'application/json' },
55
- body: JSON.stringify(Object.assign({}, body, { stream: true })),
56
- signal: controller.signal,
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ 'Accept': 'text/event-stream',
47
+ },
48
+ body: JSON.stringify(body),
49
+ signal: combinedSignal,
57
50
  });
58
-
51
+ } catch (err) {
59
52
  clearTimeout(connectTimeout);
53
+ throw err;
54
+ }
60
55
 
61
- if (res.ok === false) {
62
- let errorText = '';
63
- try {
64
- errorText = await res.text();
65
- } catch (_) {
66
- errorText = 'unknown error';
67
- }
68
- throw new Error('HTTP ' + res.status + ': ' + errorText.slice(0, 300));
56
+ clearTimeout(connectTimeout);
57
+
58
+ if (response.ok === false) {
59
+ const text = await response.text().catch(function () { return ''; });
60
+ throw new Error('HTTP ' + response.status + ': ' + text.slice(0, 200));
61
+ }
62
+
63
+ if (response.body === null || response.body === undefined) {
64
+ throw new Error('Response body is null');
65
+ }
66
+
67
+ const reader = response.body.getReader();
68
+ const decoder = new TextDecoder();
69
+ let buffer = '';
70
+ let readTimeout = null;
71
+
72
+ function resetReadTimeout() {
73
+ if (readTimeout !== null) {
74
+ clearTimeout(readTimeout);
69
75
  }
76
+ readTimeout = setTimeout(function () {
77
+ controller.abort(new Error('Read timeout: ' + READ_TIMEOUT + 'ms'));
78
+ }, READ_TIMEOUT);
79
+ }
70
80
 
71
- const reader = res.body.getReader();
72
- const decoder = new TextDecoder();
73
- let buffer = '';
81
+ resetReadTimeout();
74
82
 
83
+ try {
75
84
  while (true) {
76
- let result;
77
- try {
78
- result = await readWithTimeout(reader, READ_TIMEOUT);
79
- } catch (err) {
80
- if (err.message === 'stream_read_timeout') {
81
- yield { type: 'timeout' };
82
- return;
83
- }
84
- throw err;
85
- }
85
+ const result = await reader.read();
86
+ resetReadTimeout();
86
87
 
87
- const done = result.done;
88
- const value = result.value;
89
-
90
- if (done === true) {
91
- break;
88
+ if (result.done === true) {
89
+ yield { type: 'done' };
90
+ return;
92
91
  }
93
92
 
94
- buffer = buffer + decoder.decode(value, { stream: true });
93
+ const chunk = decoder.decode(result.value, { stream: true });
94
+ buffer = buffer + chunk;
95
95
  const lines = buffer.split('\n');
96
96
  buffer = lines.pop() || '';
97
97
 
98
98
  for (let i = 0; i < lines.length; i++) {
99
99
  const line = lines[i];
100
- if (line.startsWith('data: ') === false) {
101
- continue;
102
- }
103
- const data = line.slice(6).trim();
104
- if (data === '[DONE]') {
105
- yield { type: 'done' };
106
- return;
107
- }
108
- try {
109
- const json = JSON.parse(data);
110
- const choices = json.choices;
111
- if (choices === null || choices === undefined || choices.length < 1) {
112
- continue;
113
- }
114
- const choice = choices[0];
115
- const delta = choice.delta;
116
- if (delta === null || delta === undefined) {
117
- continue;
118
- }
119
- if (delta.content !== null && delta.content !== undefined && delta.content !== '') {
120
- yield { type: 'token', content: delta.content };
100
+ if (line.startsWith('data: ') === true) {
101
+ const data = line.slice(6).trim();
102
+ if (data === '[DONE]') {
103
+ yield { type: 'done' };
104
+ return;
121
105
  }
122
- if (choice.finish_reason !== null && choice.finish_reason !== undefined) {
123
- yield { type: 'done', reason: choice.finish_reason };
106
+ try {
107
+ const parsed = JSON.parse(data);
108
+ const choice = parsed.choices && parsed.choices[0];
109
+ if (choice !== null && choice !== undefined) {
110
+ const delta = choice.delta;
111
+ if (delta !== null && delta !== undefined) {
112
+ if (typeof delta.content === 'string') {
113
+ yield { type: 'token', content: delta.content };
114
+ }
115
+ if (typeof delta.reasoning_content === 'string') {
116
+ yield { type: 'token', content: delta.reasoning_content };
117
+ }
118
+ }
119
+ }
120
+ } catch (_) {
124
121
  }
125
- } catch (_) {
126
122
  }
127
123
  }
128
124
  }
129
-
130
- yield { type: 'done' };
125
+ } catch (err) {
126
+ if (err !== null && err !== undefined && err.name === 'AbortError') {
127
+ yield { type: 'timeout' };
128
+ return;
129
+ }
130
+ throw err;
131
131
  } finally {
132
- clearTimeout(connectTimeout);
133
- if (signal !== null && signal !== undefined) {
134
- try {
135
- signal.removeEventListener('abort', onAbort);
136
- } catch (_) {
137
- }
132
+ if (readTimeout !== null) {
133
+ clearTimeout(readTimeout);
138
134
  }
139
135
  try {
140
- controller.abort();
136
+ await reader.cancel();
141
137
  } catch (_) {
142
138
  }
143
139
  }
@@ -1,60 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex } from '../config/theme.js';
4
-
5
- const h = React.createElement;
6
-
7
- export const BRAND_HEIGHT = 5;
8
-
9
- function checkColumns(cols) {
10
- if (typeof cols !== 'number' || cols < 20) {
11
- return 80;
12
- }
13
- return Math.max(20, Math.floor(cols));
14
- }
15
-
16
- function padLine(content, innerWidth) {
17
- const safeContent = typeof content === 'string' ? content : '';
18
- const clipped = safeContent.length > innerWidth
19
- ? safeContent.slice(0, innerWidth)
20
- : safeContent;
21
- return clipped + ' '.repeat(Math.max(0, innerWidth - clipped.length));
22
- }
23
-
24
- export function createBrandHeader(cols) {
25
- const width = checkColumns(cols);
26
- const innerWidth = Math.max(1, width - 2);
27
- const rule = '\u2500'.repeat(innerWidth);
28
- const statusLine = ' [STATUS: ACTIVE] [ENGINE: 32B-OLLAMA] [WEBSEARCH: READY]';
29
-
30
- return h(Box, {
31
- flexDirection: 'column',
32
- width: '100%',
33
- height: BRAND_HEIGHT,
34
- backgroundColor: hex.black,
35
- flexShrink: 0,
36
- overflow: 'hidden',
37
- },
38
- h(Box, { height: 1 },
39
- h(Text, { color: hex.primary, bold: true }, '\u250C' + rule + '\u2510')
40
- ),
41
- h(Box, { height: 1 },
42
- h(Text, { color: hex.primary, bold: true },
43
- '\u2502' + padLine(' OPEN', innerWidth) + '\u2502'
44
- )
45
- ),
46
- h(Box, { height: 1 },
47
- h(Text, { color: hex.primary, bold: true },
48
- '\u2502' + padLine(' AXIES', innerWidth) + '\u2502'
49
- )
50
- ),
51
- h(Box, { height: 1 },
52
- h(Text, { color: hex.greenOnline, bold: true },
53
- '\u2502' + padLine(statusLine, innerWidth) + '\u2502'
54
- )
55
- ),
56
- h(Box, { height: 1 },
57
- h(Text, { color: hex.primary, bold: true }, '\u2514' + rule + '\u2518')
58
- ),
59
- );
60
- }
@@ -1,193 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import { hex } from '../config/theme.js';
4
-
5
- const h = React.createElement;
6
-
7
- const THINK_OPEN = '<think>';
8
- const THINK_CLOSE = '</think>';
9
-
10
- function checkArray(arr) {
11
- if (Array.isArray(arr) === false) {
12
- return [];
13
- }
14
- return arr;
15
- }
16
-
17
- function clampWidth(width) {
18
- if (typeof width !== 'number' || width < 20) {
19
- return 80;
20
- }
21
- return Math.max(20, width - 2);
22
- }
23
-
24
- function pushLine(lines, text, color, bold) {
25
- lines.push({
26
- text: typeof text === 'string' ? text : '',
27
- color: color,
28
- bold: bold === true,
29
- });
30
- }
31
-
32
- function wrapText(text, width) {
33
- const safeWidth = Math.max(1, width);
34
- const raw = typeof text === 'string' ? text : '';
35
- const sourceLines = raw.split('\n');
36
- const wrapped = [];
37
-
38
- for (let i = 0; i < sourceLines.length; i++) {
39
- let line = sourceLines[i];
40
- if (line.length === 0) {
41
- wrapped.push('');
42
- continue;
43
- }
44
-
45
- while (line.length > safeWidth) {
46
- let cut = line.lastIndexOf(' ', safeWidth);
47
- if (cut < Math.floor(safeWidth / 2)) {
48
- cut = safeWidth;
49
- }
50
- wrapped.push(line.slice(0, cut));
51
- line = line.slice(cut).trimStart();
52
- }
53
-
54
- wrapped.push(line);
55
- }
56
-
57
- return wrapped;
58
- }
59
-
60
- function pushWrapped(lines, text, width, color, bold) {
61
- const wrapped = wrapText(text, width);
62
- for (let i = 0; i < wrapped.length; i++) {
63
- pushLine(lines, wrapped[i], color, bold);
64
- }
65
- }
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
-
100
- function pushRawMessage(lines, msg, safeCols) {
101
- if (msg === null || msg === undefined || typeof msg !== 'object') {
102
- return;
103
- }
104
-
105
- const content = typeof msg.content === 'string' ? msg.content : '';
106
- if (content.length === 0) {
107
- return;
108
- }
109
-
110
- const clean = stripThinkTags(content);
111
- if (clean.length === 0) {
112
- return;
113
- }
114
-
115
- if (msg.role === 'user') {
116
- pushWrapped(lines, clean, safeCols, hex.neonBlue, false);
117
- return;
118
- }
119
-
120
- pushWrapped(lines, clean, safeCols, hex.primary, false);
121
- }
122
-
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) {
131
- const safeMessages = checkArray(messages);
132
- const safeAvail = typeof availLines === 'number' && availLines > 0 ? availLines : 1;
133
- const safeCols = clampWidth(cols);
134
- const rawOffset = typeof scrollOffset === 'number' && scrollOffset > 0
135
- ? Math.floor(scrollOffset)
136
- : 0;
137
- const hasStream = typeof streamText === 'string' && streamText.length > 0;
138
- const isStreamingThink = isThinking === true && hasStream === true;
139
- const transcriptLines = [];
140
-
141
- for (let i = 0; i < safeMessages.length; i++) {
142
- pushRawMessage(transcriptLines, safeMessages[i], safeCols);
143
- }
144
-
145
- if (hasStream === true) {
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
- }
158
- }
159
-
160
- if (transcriptLines.length === 0) {
161
- pushLine(transcriptLines, ' awaiting input...', '#333355', false);
162
- }
163
-
164
- const visibleHeight = Math.max(1, safeAvail - 1);
165
- const maxOffset = Math.max(0, transcriptLines.length - visibleHeight);
166
- const safeOffset = Math.min(rawOffset, maxOffset);
167
- const endIndex = transcriptLines.length - safeOffset;
168
- const startIndex = Math.max(0, endIndex - visibleHeight);
169
- const visibleLines = transcriptLines.slice(startIndex, endIndex);
170
- const elements = [];
171
-
172
- for (let i = 0; i < visibleLines.length; i++) {
173
- const line = visibleLines[i];
174
- elements.push(
175
- h(Text, {
176
- key: 'line-' + (startIndex + i),
177
- color: line.color,
178
- bold: line.bold,
179
- }, line.text)
180
- );
181
- }
182
-
183
- return h(Box, {
184
- flexGrow: 1,
185
- height: safeAvail,
186
- flexDirection: 'column',
187
- overflow: 'hidden',
188
- paddingLeft: 1,
189
- paddingRight: 1,
190
- paddingTop: 0,
191
- paddingBottom: 1,
192
- }, ...elements);
193
- }