gloomberb 0.13.1 → 0.13.2
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 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/request.ts +4 -1
- package/src/cli/pane-functions/cloud-session.test.ts +54 -0
- package/src/cli/pane-functions/cloud-session.ts +38 -0
- package/src/cli/pane-functions/index.ts +5 -1
- package/src/cli/pane-functions/screenshot.ts +2 -19
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +43 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/chat/controller/channels.ts +2 -1
- package/src/plugins/builtin/chat/controller/index.ts +13 -5
- package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
- package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
- package/src/plugins/builtin/chat/controller/state.ts +1 -0
- package/src/plugins/builtin/chat/controller.test.ts +260 -4
- package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
- package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +3 -2
- package/src/plugins/builtin/cloud/askg/protocol.ts +16 -4
- package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
- package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
- package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/congress-trades/client.ts +11 -0
- package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
- package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
- package/src/plugins/builtin/congress-trades/headless.ts +145 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
- package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
- package/src/plugins/builtin/dividend-yield/model.ts +4 -22
- package/src/plugins/builtin/dividend-yield/view.ts +17 -0
- package/src/plugins/builtin/earnings/headless.test.ts +82 -0
- package/src/plugins/builtin/earnings/headless.ts +162 -0
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/data.ts +29 -8
- package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
- package/src/plugins/builtin/earnings-calls/model.ts +238 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +8 -9
- package/src/plugins/builtin/holders/client.ts +29 -0
- package/src/plugins/builtin/holders/headless.test.ts +58 -0
- package/src/plugins/builtin/holders/headless.ts +162 -0
- package/src/plugins/builtin/holders/index.tsx +14 -8
- package/src/plugins/builtin/holders/pane.tsx +7 -1
- package/src/plugins/builtin/insider/client.ts +50 -0
- package/src/plugins/builtin/insider/headless.test.ts +122 -0
- package/src/plugins/builtin/insider/headless.ts +117 -0
- package/src/plugins/builtin/insider/index.tsx +21 -50
- package/src/plugins/builtin/insider/insider-data.ts +1 -1
- package/src/plugins/builtin/insider/model.ts +72 -0
- package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
- package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
- package/src/plugins/builtin/news/headless.test.ts +138 -0
- package/src/plugins/builtin/news/headless.ts +190 -0
- package/src/plugins/builtin/news/index.tsx +13 -9
- package/src/plugins/builtin/news/wire/index.tsx +2 -1
- package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
- package/src/plugins/builtin/research/analyst-headless.ts +172 -0
- package/src/plugins/builtin/research/analyst-model.ts +219 -0
- package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
- package/src/plugins/builtin/research/client.ts +12 -0
- package/src/plugins/builtin/research/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +28 -18
- package/src/plugins/builtin/sec/client.ts +16 -0
- package/src/plugins/builtin/sec/headless.test.ts +72 -0
- package/src/plugins/builtin/sec/headless.ts +69 -0
- package/src/plugins/builtin/sec/index.tsx +20 -75
- package/src/plugins/builtin/sec/model.ts +89 -0
- package/src/plugins/builtin/short-interest/client.ts +5 -2
- package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
- package/src/plugins/builtin/short-interest/headless.ts +111 -0
- package/src/plugins/builtin/short-interest/index.tsx +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +13 -11
- package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
- package/src/plugins/builtin/thirteenf/headless.ts +286 -0
- package/src/plugins/builtin/thirteenf/index.tsx +4 -0
- package/src/plugins/builtin/thirteenf/model.ts +13 -0
- package/src/plugins/builtin/thirteenf/table.ts +3 -15
- package/src/plugins/prediction-markets/headless.test.ts +119 -0
- package/src/plugins/prediction-markets/headless.ts +255 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/sources/gloomberb-cloud/news.ts +1 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CloudEarningsCallPayload,
|
|
3
|
+
CloudEarningsTranscriptPayload,
|
|
4
|
+
} from "../../../api-client";
|
|
5
|
+
import type {
|
|
6
|
+
HeadlessPaneContext,
|
|
7
|
+
HeadlessPaneDefinition,
|
|
8
|
+
HeadlessPaneLoadArgs,
|
|
9
|
+
HeadlessRowsResult,
|
|
10
|
+
} from "../../../types/plugin";
|
|
11
|
+
import {
|
|
12
|
+
callStatusLabel,
|
|
13
|
+
isPendingTranscript,
|
|
14
|
+
loadEarningsCallsWithClient,
|
|
15
|
+
loadTranscriptWithClient,
|
|
16
|
+
type EarningsCallsResult,
|
|
17
|
+
} from "./data";
|
|
18
|
+
import {
|
|
19
|
+
buildTranscriptSegments,
|
|
20
|
+
findCallForQuarter,
|
|
21
|
+
sortCallsNewest,
|
|
22
|
+
type TranscriptSection,
|
|
23
|
+
} from "./model";
|
|
24
|
+
|
|
25
|
+
const CALL_COLUMNS = [
|
|
26
|
+
{ key: "ticker", header: "Ticker" },
|
|
27
|
+
{ key: "company", header: "Company" },
|
|
28
|
+
{ key: "callAt", header: "Call date" },
|
|
29
|
+
{ key: "fiscalYear", header: "FY", align: "right" as const },
|
|
30
|
+
{ key: "fiscalQuarter", header: "FQ", align: "right" as const },
|
|
31
|
+
{ key: "status", header: "Status" },
|
|
32
|
+
{ key: "durationSeconds", header: "Seconds", align: "right" as const },
|
|
33
|
+
{ key: "wordCount", header: "Words", align: "right" as const },
|
|
34
|
+
{ key: "sentiment", header: "Tone", align: "right" as const },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const TRANSCRIPT_COLUMNS = [
|
|
38
|
+
{ key: "index", header: "#", align: "right" as const },
|
|
39
|
+
{ key: "section", header: "Section" },
|
|
40
|
+
{ key: "startSeconds", header: "Start", align: "right" as const },
|
|
41
|
+
{ key: "speaker", header: "Speaker" },
|
|
42
|
+
{ key: "role", header: "Role" },
|
|
43
|
+
{ key: "isQa", header: "Q&A" },
|
|
44
|
+
{ key: "text", header: "Text" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export interface EarningsCallsHeadlessDependencies {
|
|
48
|
+
loadCalls(
|
|
49
|
+
ticker: string | null,
|
|
50
|
+
limit: number,
|
|
51
|
+
context: HeadlessPaneContext,
|
|
52
|
+
): Promise<EarningsCallsResult>;
|
|
53
|
+
loadTranscript(
|
|
54
|
+
callId: string,
|
|
55
|
+
context: HeadlessPaneContext,
|
|
56
|
+
): Promise<CloudEarningsTranscriptPayload>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const defaultDependencies: EarningsCallsHeadlessDependencies = {
|
|
60
|
+
loadCalls: (ticker, limit, context) => loadEarningsCallsWithClient(
|
|
61
|
+
context.apiClient,
|
|
62
|
+
ticker,
|
|
63
|
+
{ limit },
|
|
64
|
+
),
|
|
65
|
+
loadTranscript: (callId, context) => loadTranscriptWithClient(context.apiClient, callId),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function callRow(call: CloudEarningsCallPayload) {
|
|
69
|
+
return {
|
|
70
|
+
id: call.id,
|
|
71
|
+
ticker: call.ticker,
|
|
72
|
+
company: call.companyName,
|
|
73
|
+
callAt: call.callAt,
|
|
74
|
+
fiscalYear: call.fiscalYear,
|
|
75
|
+
fiscalQuarter: call.fiscalQuarter,
|
|
76
|
+
status: call.hasTranscript ? "transcribed" : callStatusLabel(call) || call.status,
|
|
77
|
+
durationSeconds: call.durationSeconds,
|
|
78
|
+
wordCount: call.wordCount,
|
|
79
|
+
hasTranscript: call.hasTranscript,
|
|
80
|
+
sentiment: call.sentiment,
|
|
81
|
+
webcastUrl: call.webcastUrl ?? null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function availabilityMatches(call: CloudEarningsCallPayload, availability: string): boolean {
|
|
86
|
+
if (availability === "all") return true;
|
|
87
|
+
return availability === "transcribed" ? call.hasTranscript : !call.hasTranscript;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function projectEarningsCallsHeadless(
|
|
91
|
+
result: EarningsCallsResult,
|
|
92
|
+
args: HeadlessPaneLoadArgs,
|
|
93
|
+
): HeadlessRowsResult {
|
|
94
|
+
const availability = String(args.options.availability ?? "all");
|
|
95
|
+
const limit = Number(args.options.limit ?? 50);
|
|
96
|
+
const matching = sortCallsNewest(result.calls)
|
|
97
|
+
.filter((call) => availabilityMatches(call, availability));
|
|
98
|
+
const rows = matching.slice(0, limit).map(callRow);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
columns: CALL_COLUMNS,
|
|
102
|
+
rows,
|
|
103
|
+
errors: result.refreshError ? [result.refreshError] : undefined,
|
|
104
|
+
metadata: {
|
|
105
|
+
fetchedAt: result.fetchedAt,
|
|
106
|
+
stale: result.stale,
|
|
107
|
+
pending: result.pending ?? false,
|
|
108
|
+
unknownTicker: result.unknownTicker ?? false,
|
|
109
|
+
availability,
|
|
110
|
+
total: matching.length,
|
|
111
|
+
returned: rows.length,
|
|
112
|
+
truncated: rows.length < matching.length,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function selectedSection(args: HeadlessPaneLoadArgs): TranscriptSection {
|
|
118
|
+
return String(args.options.section ?? "overview") as TranscriptSection;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function transcriptResult(
|
|
122
|
+
transcript: CloudEarningsTranscriptPayload,
|
|
123
|
+
call: CloudEarningsCallPayload,
|
|
124
|
+
args: HeadlessPaneLoadArgs,
|
|
125
|
+
): HeadlessRowsResult {
|
|
126
|
+
const section = selectedSection(args);
|
|
127
|
+
const speaker = String(args.options.speaker ?? "").trim();
|
|
128
|
+
const search = String(args.options.search ?? "").trim();
|
|
129
|
+
const offset = Number(args.options.offset ?? 0);
|
|
130
|
+
const limit = Number(args.options.limit ?? 20);
|
|
131
|
+
const segments = buildTranscriptSegments(transcript, section, { speaker, search });
|
|
132
|
+
const rows = segments.slice(offset, offset + limit).map((segment) => ({ ...segment }));
|
|
133
|
+
const hasMore = offset + rows.length < segments.length;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
columns: TRANSCRIPT_COLUMNS,
|
|
137
|
+
rows,
|
|
138
|
+
metadata: {
|
|
139
|
+
callId: call.id,
|
|
140
|
+
ticker: transcript.ticker,
|
|
141
|
+
company: transcript.companyName,
|
|
142
|
+
fiscalYear: transcript.fiscalYear,
|
|
143
|
+
fiscalQuarter: transcript.fiscalQuarter,
|
|
144
|
+
callAt: transcript.callAt,
|
|
145
|
+
durationSeconds: transcript.durationSeconds,
|
|
146
|
+
wordCount: transcript.wordCount,
|
|
147
|
+
sentiment: transcript.sentiment,
|
|
148
|
+
updatedAt: transcript.updatedAt,
|
|
149
|
+
section,
|
|
150
|
+
speaker: speaker || null,
|
|
151
|
+
search: search || null,
|
|
152
|
+
offset,
|
|
153
|
+
limit,
|
|
154
|
+
totalSegments: segments.length,
|
|
155
|
+
returnedSegments: rows.length,
|
|
156
|
+
truncated: offset > 0 || hasMore,
|
|
157
|
+
hasMore,
|
|
158
|
+
nextOffset: hasMore ? offset + rows.length : null,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function transcriptOptions() {
|
|
164
|
+
return [
|
|
165
|
+
{
|
|
166
|
+
key: "section",
|
|
167
|
+
description: "Transcript section to return.",
|
|
168
|
+
type: "enum" as const,
|
|
169
|
+
values: [
|
|
170
|
+
{ value: "overview" },
|
|
171
|
+
{ value: "summary" },
|
|
172
|
+
{ value: "transcript" },
|
|
173
|
+
{ value: "qa", aliases: ["q&a"] },
|
|
174
|
+
{ value: "guidance" },
|
|
175
|
+
{ value: "risks" },
|
|
176
|
+
{ value: "notable" },
|
|
177
|
+
{ value: "analyst-focus", aliases: ["analyst"] },
|
|
178
|
+
{ value: "participants", aliases: ["speakers"] },
|
|
179
|
+
],
|
|
180
|
+
defaultValue: "overview",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
key: "quarter",
|
|
184
|
+
description: "Fiscal quarter such as latest, FQ2-2026, or FY2026.",
|
|
185
|
+
type: "string" as const,
|
|
186
|
+
defaultValue: "latest",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
key: "speaker",
|
|
190
|
+
description: "Speaker name, role, or role acronym such as CFO.",
|
|
191
|
+
type: "string" as const,
|
|
192
|
+
defaultValue: "",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
key: "search",
|
|
196
|
+
description: "Text to find in transcript turns.",
|
|
197
|
+
type: "string" as const,
|
|
198
|
+
defaultValue: "",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
key: "offset",
|
|
202
|
+
description: "Transcript segment offset for paging.",
|
|
203
|
+
type: "integer" as const,
|
|
204
|
+
defaultValue: 0,
|
|
205
|
+
minimum: 0,
|
|
206
|
+
maximum: 100_000,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
key: "limit",
|
|
210
|
+
description: "Maximum transcript segments to return.",
|
|
211
|
+
type: "integer" as const,
|
|
212
|
+
defaultValue: 20,
|
|
213
|
+
minimum: 1,
|
|
214
|
+
maximum: 200,
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function createEarningsCallsHeadless(
|
|
220
|
+
dependencies: EarningsCallsHeadlessDependencies = defaultDependencies,
|
|
221
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
222
|
+
return {
|
|
223
|
+
shape: "rows",
|
|
224
|
+
argument: {
|
|
225
|
+
kind: "ticker",
|
|
226
|
+
placeholder: "ticker",
|
|
227
|
+
description: "Optional company symbol. Omit it to browse all calls.",
|
|
228
|
+
optional: true,
|
|
229
|
+
},
|
|
230
|
+
options: [
|
|
231
|
+
{
|
|
232
|
+
key: "availability",
|
|
233
|
+
description: "Transcript availability to include.",
|
|
234
|
+
type: "enum",
|
|
235
|
+
values: [
|
|
236
|
+
{ value: "all" },
|
|
237
|
+
{ value: "transcribed" },
|
|
238
|
+
{ value: "pending" },
|
|
239
|
+
],
|
|
240
|
+
defaultValue: "all",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
key: "limit",
|
|
244
|
+
description: "Maximum calls to return.",
|
|
245
|
+
type: "integer",
|
|
246
|
+
defaultValue: 50,
|
|
247
|
+
minimum: 1,
|
|
248
|
+
maximum: 200,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
columns: CALL_COLUMNS,
|
|
252
|
+
describe: (args) => args.argument
|
|
253
|
+
? `Earnings Calls | ${String(args.argument)}`
|
|
254
|
+
: "Earnings Calls",
|
|
255
|
+
async load(args, context) {
|
|
256
|
+
const ticker = typeof args.argument === "string" ? args.argument : null;
|
|
257
|
+
return projectEarningsCallsHeadless(
|
|
258
|
+
await dependencies.loadCalls(ticker, 200, context),
|
|
259
|
+
args,
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function createEarningsTranscriptHeadless(
|
|
266
|
+
dependencies: EarningsCallsHeadlessDependencies = defaultDependencies,
|
|
267
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
268
|
+
return {
|
|
269
|
+
shape: "rows",
|
|
270
|
+
argument: {
|
|
271
|
+
kind: "ticker",
|
|
272
|
+
placeholder: "ticker",
|
|
273
|
+
description: "Company symbol.",
|
|
274
|
+
},
|
|
275
|
+
options: transcriptOptions(),
|
|
276
|
+
columns: TRANSCRIPT_COLUMNS,
|
|
277
|
+
describe: (args) => (
|
|
278
|
+
`Earnings Call Transcript | ${String(args.argument)} | ${String(args.options.quarter)}`
|
|
279
|
+
),
|
|
280
|
+
async load(args, context) {
|
|
281
|
+
const ticker = String(args.argument);
|
|
282
|
+
const calls = await dependencies.loadCalls(ticker, 200, context);
|
|
283
|
+
const quarter = String(args.options.quarter ?? "latest");
|
|
284
|
+
const selected = findCallForQuarter(calls.calls, quarter);
|
|
285
|
+
if (!selected) {
|
|
286
|
+
if (calls.unknownTicker) throw new Error(`${ticker} is not a known listed company.`);
|
|
287
|
+
if (calls.pending) throw new Error(`Earnings call discovery for ${ticker} is still pending.`);
|
|
288
|
+
throw new Error(`No ${quarter} earnings call found for ${ticker}.`);
|
|
289
|
+
}
|
|
290
|
+
const transcript = await dependencies.loadTranscript(selected.id, context);
|
|
291
|
+
if (isPendingTranscript(transcript)) {
|
|
292
|
+
throw new Error(`Transcript for ${ticker} ${quarter} is still being produced.`);
|
|
293
|
+
}
|
|
294
|
+
return transcriptResult(transcript, selected, args);
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export const earningsCallsHeadless = createEarningsCallsHeadless();
|
|
300
|
+
export const earningsTranscriptHeadless = createEarningsTranscriptHeadless();
|
|
@@ -2,6 +2,12 @@ import type { PluginModule } from "../plugin-module";
|
|
|
2
2
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
3
3
|
import { attachEarningsCallsPersistence, resetEarningsCallsPersistence } from "./data";
|
|
4
4
|
import { EarningsCallsPane, EARNINGS_CALLS_PANE_ID } from "./pane";
|
|
5
|
+
import {
|
|
6
|
+
earningsCallsHeadless,
|
|
7
|
+
earningsTranscriptHeadless,
|
|
8
|
+
} from "./headless";
|
|
9
|
+
|
|
10
|
+
export { earningsCallsHeadless, earningsTranscriptHeadless } from "./headless";
|
|
5
11
|
|
|
6
12
|
const description =
|
|
7
13
|
"Earnings call transcripts with speaker attribution, analyst Q&A, and extracted guidance.";
|
|
@@ -54,16 +60,20 @@ export const earningsCallsModule: PluginModule = {
|
|
|
54
60
|
"qa",
|
|
55
61
|
],
|
|
56
62
|
shortcut: { prefix: "CALLS" },
|
|
63
|
+
headless: earningsCallsHeadless,
|
|
57
64
|
createInstance: () => ({ placement: "floating" }),
|
|
58
65
|
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
{
|
|
67
|
+
...createTickerSurfacePaneTemplate({
|
|
68
|
+
id: "earnings-call-transcripts-pane",
|
|
69
|
+
paneId: EARNINGS_CALLS_PANE_ID,
|
|
70
|
+
label: "Earnings Call Transcripts",
|
|
71
|
+
description,
|
|
72
|
+
keywords: ["earnings", "call", "transcript", "ect", "qa", "guidance"],
|
|
73
|
+
shortcut: "ECT",
|
|
74
|
+
publicShare: false,
|
|
75
|
+
}),
|
|
76
|
+
headless: earningsTranscriptHeadless,
|
|
77
|
+
},
|
|
68
78
|
],
|
|
69
79
|
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CloudEarningsCallPayload,
|
|
3
|
+
CloudEarningsTranscriptPayload,
|
|
4
|
+
CloudTranscriptTurnPayload,
|
|
5
|
+
} from "../../../api-client";
|
|
6
|
+
|
|
7
|
+
export type TranscriptSection =
|
|
8
|
+
| "overview"
|
|
9
|
+
| "summary"
|
|
10
|
+
| "transcript"
|
|
11
|
+
| "qa"
|
|
12
|
+
| "guidance"
|
|
13
|
+
| "risks"
|
|
14
|
+
| "notable"
|
|
15
|
+
| "analyst-focus"
|
|
16
|
+
| "participants";
|
|
17
|
+
|
|
18
|
+
export interface TranscriptFilter {
|
|
19
|
+
section: "transcript" | "qa";
|
|
20
|
+
speaker?: string;
|
|
21
|
+
search?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TranscriptSegment {
|
|
25
|
+
index: number;
|
|
26
|
+
section: string;
|
|
27
|
+
startSeconds: number | null;
|
|
28
|
+
speaker: string | null;
|
|
29
|
+
role: string | null;
|
|
30
|
+
company: string | null;
|
|
31
|
+
isQa: boolean;
|
|
32
|
+
text: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const MAX_SEGMENT_CHARS = 1_600;
|
|
36
|
+
|
|
37
|
+
function roleAcronym(value: string): string {
|
|
38
|
+
return value
|
|
39
|
+
.split(/\s+/)
|
|
40
|
+
.filter((word) => /^[A-Za-z]/.test(word))
|
|
41
|
+
.map((word) => word[0])
|
|
42
|
+
.join("");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function turnSpeakerText(turn: CloudTranscriptTurnPayload): string {
|
|
46
|
+
const role = turn.role ?? "";
|
|
47
|
+
return [
|
|
48
|
+
turn.speaker,
|
|
49
|
+
role,
|
|
50
|
+
roleAcronym(role),
|
|
51
|
+
turn.company ?? "",
|
|
52
|
+
].join(" ").toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function turnSearchText(turn: CloudTranscriptTurnPayload): string {
|
|
56
|
+
return `${turnSpeakerText(turn)} ${turn.text}`.toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function filterTranscriptTurns(
|
|
60
|
+
turns: CloudTranscriptTurnPayload[],
|
|
61
|
+
filter: TranscriptFilter,
|
|
62
|
+
): CloudTranscriptTurnPayload[] {
|
|
63
|
+
const speaker = filter.speaker?.trim().toLowerCase() ?? "";
|
|
64
|
+
const search = filter.search?.trim().toLowerCase() ?? "";
|
|
65
|
+
return turns.filter((turn) => {
|
|
66
|
+
if (filter.section === "qa" && !turn.isQa) return false;
|
|
67
|
+
if (speaker && !turnSpeakerText(turn).includes(speaker)) return false;
|
|
68
|
+
return !search || turnSearchText(turn).includes(search);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function chunks(text: string): string[] {
|
|
73
|
+
const normalized = text.trim();
|
|
74
|
+
if (!normalized) return [];
|
|
75
|
+
const result: string[] = [];
|
|
76
|
+
let remaining = normalized;
|
|
77
|
+
while (remaining.length > MAX_SEGMENT_CHARS) {
|
|
78
|
+
const breakAt = Math.max(
|
|
79
|
+
remaining.lastIndexOf(" ", MAX_SEGMENT_CHARS),
|
|
80
|
+
remaining.lastIndexOf("\n", MAX_SEGMENT_CHARS),
|
|
81
|
+
);
|
|
82
|
+
const cut = breakAt > MAX_SEGMENT_CHARS / 2 ? breakAt : MAX_SEGMENT_CHARS;
|
|
83
|
+
result.push(remaining.slice(0, cut).trim());
|
|
84
|
+
remaining = remaining.slice(cut).trim();
|
|
85
|
+
}
|
|
86
|
+
if (remaining) result.push(remaining);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function summaryFields(
|
|
91
|
+
transcript: CloudEarningsTranscriptPayload,
|
|
92
|
+
section: TranscriptSection,
|
|
93
|
+
): Array<{ section: string; text: string }> {
|
|
94
|
+
const fields = [
|
|
95
|
+
{ section: "summary", text: transcript.summary ?? "" },
|
|
96
|
+
{ section: "notable", text: transcript.notable ?? "" },
|
|
97
|
+
{ section: "analyst-focus", text: transcript.analystFocus ?? "" },
|
|
98
|
+
{ section: "guidance", text: transcript.guidance ?? "" },
|
|
99
|
+
{ section: "risks", text: transcript.riskFactors ?? "" },
|
|
100
|
+
];
|
|
101
|
+
return section === "overview"
|
|
102
|
+
? fields.filter((field) => field.text.trim())
|
|
103
|
+
: fields.filter((field) => field.section === section && field.text.trim());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function buildTranscriptSegments(
|
|
107
|
+
transcript: CloudEarningsTranscriptPayload,
|
|
108
|
+
section: TranscriptSection,
|
|
109
|
+
options: { speaker?: string; search?: string } = {},
|
|
110
|
+
): TranscriptSegment[] {
|
|
111
|
+
const withoutIndexes: Array<Omit<TranscriptSegment, "index">> = [];
|
|
112
|
+
|
|
113
|
+
if (section === "transcript" || section === "qa") {
|
|
114
|
+
const turns = filterTranscriptTurns(transcript.turns ?? [], {
|
|
115
|
+
section,
|
|
116
|
+
speaker: options.speaker,
|
|
117
|
+
search: options.search,
|
|
118
|
+
});
|
|
119
|
+
for (const turn of turns) {
|
|
120
|
+
for (const text of chunks(turn.text)) {
|
|
121
|
+
withoutIndexes.push({
|
|
122
|
+
section,
|
|
123
|
+
startSeconds: turn.startSeconds,
|
|
124
|
+
speaker: turn.speaker,
|
|
125
|
+
role: turn.role ?? null,
|
|
126
|
+
company: turn.company ?? null,
|
|
127
|
+
isQa: turn.isQa,
|
|
128
|
+
text,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (withoutIndexes.length === 0 && section === "transcript" && transcript.fullText.trim()) {
|
|
133
|
+
const search = options.search?.trim().toLowerCase() ?? "";
|
|
134
|
+
if (!options.speaker?.trim() && (!search || transcript.fullText.toLowerCase().includes(search))) {
|
|
135
|
+
for (const paragraph of transcript.fullText.split(/\n{2,}/)) {
|
|
136
|
+
for (const text of chunks(paragraph)) {
|
|
137
|
+
withoutIndexes.push({
|
|
138
|
+
section,
|
|
139
|
+
startSeconds: null,
|
|
140
|
+
speaker: null,
|
|
141
|
+
role: null,
|
|
142
|
+
company: null,
|
|
143
|
+
isQa: false,
|
|
144
|
+
text,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else if (section === "participants") {
|
|
151
|
+
const speaker = options.speaker?.trim().toLowerCase() ?? "";
|
|
152
|
+
for (const participant of transcript.participants) {
|
|
153
|
+
const haystack = [
|
|
154
|
+
participant.name,
|
|
155
|
+
participant.role ?? "",
|
|
156
|
+
roleAcronym(participant.role ?? ""),
|
|
157
|
+
participant.company ?? "",
|
|
158
|
+
].join(" ").toLowerCase();
|
|
159
|
+
if (speaker && !haystack.includes(speaker)) continue;
|
|
160
|
+
withoutIndexes.push({
|
|
161
|
+
section,
|
|
162
|
+
startSeconds: null,
|
|
163
|
+
speaker: participant.name,
|
|
164
|
+
role: participant.role ?? null,
|
|
165
|
+
company: participant.company ?? null,
|
|
166
|
+
isQa: false,
|
|
167
|
+
text: [participant.role, participant.company].filter(Boolean).join(", "),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
for (const field of summaryFields(transcript, section)) {
|
|
172
|
+
for (const text of chunks(field.text)) {
|
|
173
|
+
withoutIndexes.push({
|
|
174
|
+
section: field.section,
|
|
175
|
+
startSeconds: null,
|
|
176
|
+
speaker: null,
|
|
177
|
+
role: null,
|
|
178
|
+
company: null,
|
|
179
|
+
isQa: false,
|
|
180
|
+
text,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return withoutIndexes.map((segment, index) => ({ index, ...segment }));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function callTime(call: CloudEarningsCallPayload): number {
|
|
190
|
+
const timestamp = call.callAt ? Date.parse(call.callAt) : Number.NaN;
|
|
191
|
+
if (Number.isFinite(timestamp)) return timestamp;
|
|
192
|
+
return (call.fiscalYear ?? 0) * 10 + (call.fiscalQuarter ?? 0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function sortCallsNewest(calls: CloudEarningsCallPayload[]): CloudEarningsCallPayload[] {
|
|
196
|
+
return [...calls].sort((left, right) => callTime(right) - callTime(left));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sameYear(actual: number | null, requested: number | null): boolean {
|
|
200
|
+
if (requested == null) return true;
|
|
201
|
+
if (actual == null) return false;
|
|
202
|
+
return requested < 100 ? actual % 100 === requested : actual === requested;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function findCallForQuarter(
|
|
206
|
+
calls: CloudEarningsCallPayload[],
|
|
207
|
+
requestedQuarter: string,
|
|
208
|
+
): CloudEarningsCallPayload | null {
|
|
209
|
+
const sorted = sortCallsNewest(calls);
|
|
210
|
+
const token = requestedQuarter.trim().toUpperCase().replace(/[\s_-]+/g, "");
|
|
211
|
+
if (!token || token === "LATEST") return sorted[0] ?? null;
|
|
212
|
+
|
|
213
|
+
const quarterFirst = token.match(/^(?:F?Q)?([1-4])(?:\/?(\d{2}|\d{4}))?$/);
|
|
214
|
+
if (quarterFirst) {
|
|
215
|
+
const quarter = Number(quarterFirst[1]);
|
|
216
|
+
const year = quarterFirst[2] ? Number(quarterFirst[2]) : null;
|
|
217
|
+
return sorted.find((call) => (
|
|
218
|
+
call.fiscalQuarter === quarter && sameYear(call.fiscalYear, year)
|
|
219
|
+
)) ?? null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const yearFirst = token.match(/^(\d{2}|\d{4})(?:\/?F?Q)([1-4])$/);
|
|
223
|
+
if (yearFirst) {
|
|
224
|
+
const year = Number(yearFirst[1]);
|
|
225
|
+
const quarter = Number(yearFirst[2]);
|
|
226
|
+
return sorted.find((call) => (
|
|
227
|
+
call.fiscalQuarter === quarter && sameYear(call.fiscalYear, year)
|
|
228
|
+
)) ?? null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const fiscalYear = token.match(/^(?:FY)?(\d{2}|\d{4})$/);
|
|
232
|
+
if (fiscalYear) {
|
|
233
|
+
const year = Number(fiscalYear[1]);
|
|
234
|
+
return sorted.find((call) => sameYear(call.fiscalYear, year)) ?? null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
} from "../../../api-client";
|
|
19
19
|
import { formatCallDate, formatDuration, formatSentiment, formatTimestamp } from "./format";
|
|
20
20
|
import { splitFigures, splitSentences } from "./prose";
|
|
21
|
+
import { filterTranscriptTurns } from "./model";
|
|
21
22
|
|
|
22
23
|
export type ReaderTab = "summary" | "transcript" | "qa";
|
|
23
24
|
|
|
@@ -200,15 +201,13 @@ export function TranscriptView({
|
|
|
200
201
|
const { nativePaneChrome } = useUiCapabilities();
|
|
201
202
|
const isNative = nativePaneChrome === true;
|
|
202
203
|
|
|
203
|
-
const turns = useMemo(() =>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
);
|
|
211
|
-
}, [transcript, tab, query]);
|
|
204
|
+
const turns = useMemo(() => filterTranscriptTurns(
|
|
205
|
+
transcript?.turns ?? [],
|
|
206
|
+
{
|
|
207
|
+
section: tab === "qa" ? "qa" : "transcript",
|
|
208
|
+
search: query,
|
|
209
|
+
},
|
|
210
|
+
), [transcript, tab, query]);
|
|
212
211
|
|
|
213
212
|
if (loading && !transcript) {
|
|
214
213
|
return (
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DataProvider, MarketDataRequestContext } from "../../../types/data-provider";
|
|
2
|
+
import type { HolderData } from "../../../types/financials";
|
|
3
|
+
|
|
4
|
+
export async function loadHolderData(
|
|
5
|
+
provider: DataProvider,
|
|
6
|
+
symbol: string,
|
|
7
|
+
exchange = "",
|
|
8
|
+
context?: MarketDataRequestContext,
|
|
9
|
+
): Promise<HolderData> {
|
|
10
|
+
if (!provider.getHolders) throw new Error("Holder data unavailable");
|
|
11
|
+
return provider.getHolders(symbol, exchange, context);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function loadHolderSnapshot(
|
|
15
|
+
provider: DataProvider,
|
|
16
|
+
symbol: string,
|
|
17
|
+
exchange = "",
|
|
18
|
+
): Promise<{ data: HolderData; marketCap?: number }> {
|
|
19
|
+
const [data, financials] = await Promise.all([
|
|
20
|
+
loadHolderData(provider, symbol, exchange),
|
|
21
|
+
provider.getTickerFinancials(symbol, exchange).catch(() => null),
|
|
22
|
+
]);
|
|
23
|
+
const quoteMarketCap = financials?.quote?.marketCap;
|
|
24
|
+
const marketCap = financials?.quote?.currency && data.currency
|
|
25
|
+
&& financials.quote.currency !== data.currency
|
|
26
|
+
? undefined
|
|
27
|
+
: quoteMarketCap;
|
|
28
|
+
return { data, marketCap };
|
|
29
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createTestDataProvider } from "../../../test-support/data-provider";
|
|
3
|
+
import { createDefaultConfig } from "../../../types/config";
|
|
4
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
5
|
+
import { createHoldersHeadless } from "./headless";
|
|
6
|
+
|
|
7
|
+
function context(): HeadlessPaneContext {
|
|
8
|
+
return {
|
|
9
|
+
marketData: createTestDataProvider(),
|
|
10
|
+
apiClient: {} as HeadlessPaneContext["apiClient"],
|
|
11
|
+
config: createDefaultConfig("/tmp/gloomberb-headless-holders"),
|
|
12
|
+
signal: new AbortController().signal,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function args(overrides: Partial<HeadlessPaneLoadArgs["options"]> = {}): HeadlessPaneLoadArgs {
|
|
17
|
+
return {
|
|
18
|
+
rawArgument: "NVDA",
|
|
19
|
+
argument: "NVDA",
|
|
20
|
+
symbols: ["NVDA"],
|
|
21
|
+
options: { sort: "value", order: "desc", limit: 25, ...overrides },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("holders headless model", () => {
|
|
26
|
+
test("projects holder values and applies sorting and limits", async () => {
|
|
27
|
+
const headless = createHoldersHeadless({
|
|
28
|
+
loadSnapshot: async () => ({
|
|
29
|
+
marketCap: 1_000,
|
|
30
|
+
data: {
|
|
31
|
+
symbol: "NVDA",
|
|
32
|
+
currency: "USD",
|
|
33
|
+
asOf: "2026-06-30",
|
|
34
|
+
holders: [
|
|
35
|
+
{ ownerType: "institution", name: "Small Fund", value: 100, shares: 4, reportDate: "2026-06-30" },
|
|
36
|
+
{ ownerType: "fund", name: "Large Fund", value: 300, shares: 8, changeShares: 2, reportDate: "2026-06-30" },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const byValue = await headless.load(args({ limit: 1 }), context());
|
|
43
|
+
expect(byValue.rows).toEqual([{
|
|
44
|
+
name: "Large Fund",
|
|
45
|
+
ownerType: "fund",
|
|
46
|
+
value: 300,
|
|
47
|
+
shares: 8,
|
|
48
|
+
changeShares: 2,
|
|
49
|
+
changePercent: null,
|
|
50
|
+
percentHeld: 0.3,
|
|
51
|
+
reportDate: "2026-06-30",
|
|
52
|
+
currency: "USD",
|
|
53
|
+
}]);
|
|
54
|
+
|
|
55
|
+
const byName = await headless.load(args({ sort: "holder", order: "asc", limit: 2 }), context());
|
|
56
|
+
expect(byName.rows.map((row) => row.name)).toEqual(["Large Fund", "Small Fund"]);
|
|
57
|
+
});
|
|
58
|
+
});
|