ei-tui 0.1.3 → 0.1.5
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/README.md +36 -35
- package/package.json +6 -2
- package/src/README.md +85 -1
- package/src/cli/README.md +30 -20
- package/src/cli/retrieval.ts +5 -17
- package/src/cli.ts +69 -0
- package/src/core/handlers/index.ts +195 -172
- package/src/core/orchestrators/ceremony.ts +4 -4
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/processor.ts +245 -77
- package/src/core/queue-processor.ts +3 -26
- package/src/core/state/checkpoints.ts +4 -0
- package/src/core/state/personas.ts +13 -1
- package/src/core/state/queue.ts +80 -23
- package/src/core/state-manager.ts +36 -10
- package/src/core/types.ts +23 -11
- package/src/core/utils/crossFind.ts +44 -0
- package/src/core/utils/index.ts +4 -0
- package/src/integrations/opencode/importer.ts +118 -691
- package/src/prompts/heartbeat/check.ts +27 -13
- package/src/prompts/heartbeat/ei.ts +65 -136
- package/src/prompts/heartbeat/types.ts +47 -17
- package/src/prompts/human/item-update.ts +20 -8
- package/src/prompts/index.ts +2 -5
- package/src/prompts/message-utils.ts +42 -3
- package/src/prompts/response/index.ts +13 -6
- package/src/prompts/response/sections.ts +65 -12
- package/src/prompts/response/types.ts +10 -0
- package/tui/README.md +89 -4
- package/tui/src/commands/dlq.ts +75 -0
- package/tui/src/commands/editor.tsx +1 -1
- package/tui/src/commands/queue.ts +77 -0
- package/tui/src/components/CommandSuggest.tsx +50 -0
- package/tui/src/components/MessageList.tsx +12 -2
- package/tui/src/components/PromptInput.tsx +118 -30
- package/tui/src/components/Sidebar.tsx +6 -2
- package/tui/src/components/StatusBar.tsx +12 -5
- package/tui/src/context/ei.tsx +43 -3
- package/tui/src/context/keyboard.tsx +90 -2
- package/tui/src/util/clipboard.ts +73 -0
- package/tui/src/util/yaml-serializers.ts +81 -11
- package/src/prompts/validation/ei.ts +0 -93
- package/src/prompts/validation/index.ts +0 -6
- package/src/prompts/validation/types.ts +0 -22
package/src/core/state/queue.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { LLMRequest,
|
|
1
|
+
import type { LLMRequest, QueueFailResult } from "../types.js";
|
|
2
|
+
import { DLQ_MAX_COUNT, DLQ_MAX_AGE_DAYS } from "../types.js";
|
|
2
3
|
|
|
3
4
|
const BASE_BACKOFF_MS = 2_000;
|
|
4
5
|
const MAX_BACKOFF_MS = 30_000;
|
|
@@ -27,35 +28,55 @@ export class QueueState {
|
|
|
27
28
|
private paused = false;
|
|
28
29
|
|
|
29
30
|
load(queue: LLMRequest[]): void {
|
|
30
|
-
|
|
31
|
+
// Reset any items stuck in 'processing' from a previous session
|
|
32
|
+
this.queue = queue.map(r =>
|
|
33
|
+
r.state === "processing" ? { ...r, state: "pending" } : r
|
|
34
|
+
);
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export(): LLMRequest[] {
|
|
34
38
|
return this.queue;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts">): string {
|
|
41
|
+
enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts" | "state">): string {
|
|
38
42
|
const id = crypto.randomUUID();
|
|
39
43
|
const fullRequest: LLMRequest = {
|
|
40
44
|
...request,
|
|
41
45
|
id,
|
|
42
46
|
created_at: new Date().toISOString(),
|
|
43
47
|
attempts: 0,
|
|
48
|
+
state: "pending",
|
|
44
49
|
};
|
|
45
50
|
this.queue.push(fullRequest);
|
|
46
51
|
return id;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
claimHighest(): LLMRequest | null {
|
|
50
55
|
if (this.paused || this.queue.length === 0) return null;
|
|
51
|
-
const
|
|
52
|
-
const available = this.queue.filter(r => !r.retry_after || r.retry_after <= now);
|
|
56
|
+
const available = this.queue.filter(r => r.state === "pending");
|
|
53
57
|
if (available.length === 0) return null;
|
|
54
58
|
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
|
55
59
|
const sorted = [...available].sort(
|
|
56
60
|
(a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]
|
|
57
61
|
);
|
|
58
|
-
|
|
62
|
+
const item = sorted[0] ?? null;
|
|
63
|
+
if (item) {
|
|
64
|
+
item.state = "processing";
|
|
65
|
+
}
|
|
66
|
+
return item;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Returns the retry_after of the highest-priority pending item, or null if ready now.
|
|
70
|
+
// Used by the processor loop to decide whether to sleep instead of claiming.
|
|
71
|
+
nextItemRetryAfter(): string | null {
|
|
72
|
+
if (this.paused || this.queue.length === 0) return null;
|
|
73
|
+
const available = this.queue.filter(r => r.state === "pending");
|
|
74
|
+
if (available.length === 0) return null;
|
|
75
|
+
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
|
76
|
+
const sorted = [...available].sort(
|
|
77
|
+
(a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]
|
|
78
|
+
);
|
|
79
|
+
return sorted[0]?.retry_after ?? null;
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
complete(id: string): void {
|
|
@@ -76,38 +97,32 @@ export class QueueState {
|
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
// No error string and not flagged permanent = just increment, no classification
|
|
100
|
+
// Still reset to pending in case it was claimed (processing state)
|
|
79
101
|
if (!error && !permanent) {
|
|
102
|
+
request.state = "pending";
|
|
80
103
|
return { dropped: false };
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
const shouldDrop = permanent || (error ? isPermanentError(error) : false);
|
|
84
107
|
|
|
85
108
|
if (shouldDrop) {
|
|
86
|
-
|
|
109
|
+
request.state = "dlq";
|
|
87
110
|
return { dropped: true };
|
|
88
111
|
}
|
|
89
112
|
|
|
90
|
-
// Transient error —
|
|
113
|
+
// Transient error — reset to pending with backoff timer
|
|
114
|
+
request.state = "pending";
|
|
91
115
|
const delay = calculateBackoff(request.attempts);
|
|
92
116
|
request.retry_after = new Date(Date.now() + delay).toISOString();
|
|
93
117
|
return { dropped: false, retryDelay: delay };
|
|
94
118
|
}
|
|
95
119
|
|
|
96
|
-
getValidations(): LLMRequest[] {
|
|
97
|
-
return this.queue.filter(
|
|
98
|
-
(r) => r.next_step === ("handleEiValidation" as LLMNextStep)
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
120
|
|
|
102
|
-
clearValidations(ids: string[]): void {
|
|
103
|
-
const idSet = new Set(ids);
|
|
104
|
-
this.queue = this.queue.filter((r) => !idSet.has(r.id));
|
|
105
|
-
}
|
|
106
121
|
|
|
107
122
|
clearPersonaResponses(personaId: string, nextStep: string): string[] {
|
|
108
123
|
const removedIds: string[] = [];
|
|
109
124
|
this.queue = this.queue.filter((r) => {
|
|
110
|
-
if (r.next_step === nextStep && r.data.personaId === personaId) {
|
|
125
|
+
if (r.state !== "dlq" && r.next_step === nextStep && r.data.personaId === personaId) {
|
|
111
126
|
removedIds.push(r.id);
|
|
112
127
|
return false;
|
|
113
128
|
}
|
|
@@ -117,7 +132,49 @@ export class QueueState {
|
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
length(): number {
|
|
120
|
-
return this.queue.length;
|
|
135
|
+
return this.queue.filter(r => r.state === "pending" || r.state === "processing").length;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
dlqLength(): number {
|
|
139
|
+
return this.queue.filter(r => r.state === "dlq").length;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
hasProcessingItem(): boolean {
|
|
143
|
+
return this.queue.some(r => r.state === "processing");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getDLQItems(): LLMRequest[] {
|
|
147
|
+
return this.queue.filter(r => r.state === "dlq");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getAllActiveItems(): LLMRequest[] {
|
|
151
|
+
return this.queue.filter(r => r.state !== "dlq");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
updateItem(id: string, updates: Partial<LLMRequest>): boolean {
|
|
155
|
+
const idx = this.queue.findIndex(r => r.id === id);
|
|
156
|
+
if (idx < 0) return false;
|
|
157
|
+
this.queue[idx] = { ...this.queue[idx], ...updates };
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
trimDLQ(): number {
|
|
162
|
+
const dlqItems = this.queue.filter(r => r.state === "dlq");
|
|
163
|
+
const cutoff = new Date();
|
|
164
|
+
cutoff.setDate(cutoff.getDate() - DLQ_MAX_AGE_DAYS);
|
|
165
|
+
const cutoffISO = cutoff.toISOString();
|
|
166
|
+
|
|
167
|
+
// Remove by age first
|
|
168
|
+
const afterAgeTrim = dlqItems.filter(r => r.created_at >= cutoffISO);
|
|
169
|
+
|
|
170
|
+
// Then enforce count limit (oldest first)
|
|
171
|
+
const sorted = afterAgeTrim.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
172
|
+
const kept = sorted.slice(-DLQ_MAX_COUNT);
|
|
173
|
+
const keptIds = new Set(kept.map(r => r.id));
|
|
174
|
+
|
|
175
|
+
const before = this.queue.length;
|
|
176
|
+
this.queue = this.queue.filter(r => r.state !== "dlq" || keptIds.has(r.id));
|
|
177
|
+
return before - this.queue.length;
|
|
121
178
|
}
|
|
122
179
|
|
|
123
180
|
pause(): void {
|
|
@@ -133,12 +190,12 @@ export class QueueState {
|
|
|
133
190
|
}
|
|
134
191
|
|
|
135
192
|
hasPendingCeremonies(): boolean {
|
|
136
|
-
return this.queue.some(r => r.data.ceremony_progress === true);
|
|
193
|
+
return this.queue.some(r => r.state !== "dlq" && r.data.ceremony_progress === true);
|
|
137
194
|
}
|
|
138
195
|
|
|
139
196
|
clear(): number {
|
|
140
|
-
const count = this.queue.length;
|
|
141
|
-
this.queue =
|
|
197
|
+
const count = this.queue.filter(r => r.state !== "dlq").length;
|
|
198
|
+
this.queue = this.queue.filter(r => r.state === "dlq");
|
|
142
199
|
return count;
|
|
143
200
|
}
|
|
144
201
|
}
|
|
@@ -250,7 +250,7 @@ export class StateManager {
|
|
|
250
250
|
return result;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
queue_enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts">): string {
|
|
253
|
+
queue_enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts" | "state">): string {
|
|
254
254
|
const requestWithModel = {
|
|
255
255
|
...request,
|
|
256
256
|
model: request.model ?? this.humanState.get().settings?.default_model,
|
|
@@ -260,8 +260,8 @@ export class StateManager {
|
|
|
260
260
|
return id;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
return this.queueState.
|
|
263
|
+
queue_claimHighest(): LLMRequest | null {
|
|
264
|
+
return this.queueState.claimHighest();
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
queue_complete(id: string): void {
|
|
@@ -275,14 +275,7 @@ export class StateManager {
|
|
|
275
275
|
return result;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
queue_getValidations(): LLMRequest[] {
|
|
279
|
-
return this.queueState.getValidations();
|
|
280
|
-
}
|
|
281
278
|
|
|
282
|
-
queue_clearValidations(ids: string[]): void {
|
|
283
|
-
this.queueState.clearValidations(ids);
|
|
284
|
-
this.scheduleSave();
|
|
285
|
-
}
|
|
286
279
|
|
|
287
280
|
queue_clearPersonaResponses(personaId: string, nextStep: string): string[] {
|
|
288
281
|
const result = this.queueState.clearPersonaResponses(personaId, nextStep);
|
|
@@ -294,6 +287,14 @@ export class StateManager {
|
|
|
294
287
|
return this.queueState.length();
|
|
295
288
|
}
|
|
296
289
|
|
|
290
|
+
queue_hasProcessingItem(): boolean {
|
|
291
|
+
return this.queueState.hasProcessingItem();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
queue_nextItemRetryAfter(): string | null {
|
|
295
|
+
return this.queueState.nextItemRetryAfter();
|
|
296
|
+
}
|
|
297
|
+
|
|
297
298
|
queue_pause(): void {
|
|
298
299
|
this.queueState.pause();
|
|
299
300
|
this.scheduleSave();
|
|
@@ -318,6 +319,30 @@ export class StateManager {
|
|
|
318
319
|
return result;
|
|
319
320
|
}
|
|
320
321
|
|
|
322
|
+
queue_dlqLength(): number {
|
|
323
|
+
return this.queueState.dlqLength();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
queue_getDLQItems(): LLMRequest[] {
|
|
327
|
+
return this.queueState.getDLQItems();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
queue_getAllActiveItems(): LLMRequest[] {
|
|
331
|
+
return this.queueState.getAllActiveItems();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
queue_updateItem(id: string, updates: Partial<LLMRequest>): boolean {
|
|
335
|
+
const result = this.queueState.updateItem(id, updates);
|
|
336
|
+
if (result) this.scheduleSave();
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
queue_trimDLQ(): number {
|
|
341
|
+
const result = this.queueState.trimDLQ();
|
|
342
|
+
if (result > 0) this.scheduleSave();
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
|
|
321
346
|
async flush(): Promise<void> {
|
|
322
347
|
await this.persistenceState.flush();
|
|
323
348
|
}
|
|
@@ -338,6 +363,7 @@ export class StateManager {
|
|
|
338
363
|
this.humanState.load(state.human);
|
|
339
364
|
this.personaState.load(state.personas);
|
|
340
365
|
this.queueState.load(state.queue);
|
|
366
|
+
this.persistenceState.markExistingData();
|
|
341
367
|
this.scheduleSave();
|
|
342
368
|
}
|
|
343
369
|
|
package/src/core/types.ts
CHANGED
|
@@ -18,7 +18,6 @@ export enum ValidationLevel {
|
|
|
18
18
|
Ei = "ei", // Ei mentioned it to user (don't mention again)
|
|
19
19
|
Human = "human", // User explicitly confirmed (locked)
|
|
20
20
|
}
|
|
21
|
-
|
|
22
21
|
export enum LLMRequestType {
|
|
23
22
|
Response = "response",
|
|
24
23
|
JSON = "json",
|
|
@@ -47,9 +46,7 @@ export enum LLMNextStep {
|
|
|
47
46
|
HandlePersonaTopicUpdate = "handlePersonaTopicUpdate",
|
|
48
47
|
HandleHeartbeatCheck = "handleHeartbeatCheck",
|
|
49
48
|
HandleEiHeartbeat = "handleEiHeartbeat",
|
|
50
|
-
HandleEiValidation = "handleEiValidation",
|
|
51
49
|
HandleOneShot = "handleOneShot",
|
|
52
|
-
// Ceremony handlers
|
|
53
50
|
HandlePersonaExpire = "handlePersonaExpire",
|
|
54
51
|
HandlePersonaExplore = "handlePersonaExplore",
|
|
55
52
|
HandleDescriptionCheck = "handleDescriptionCheck",
|
|
@@ -83,6 +80,7 @@ export interface Topic extends DataItemBase {
|
|
|
83
80
|
category?: string; // Interest, Goal, Dream, Conflict, Concern, Fear, Hope, Plan, Project
|
|
84
81
|
exposure_current: number;
|
|
85
82
|
exposure_desired: number;
|
|
83
|
+
last_ei_asked?: string | null; // ISO timestamp of last time Ei proactively asked about this
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
/**
|
|
@@ -109,6 +107,7 @@ export interface Person extends DataItemBase {
|
|
|
109
107
|
relationship: string;
|
|
110
108
|
exposure_current: number;
|
|
111
109
|
exposure_desired: number;
|
|
110
|
+
last_ei_asked?: string | null; // ISO timestamp of last time Ei proactively asked about this
|
|
112
111
|
}
|
|
113
112
|
|
|
114
113
|
export interface Quote {
|
|
@@ -180,7 +179,8 @@ export interface OpenCodeSettings {
|
|
|
180
179
|
integration?: boolean;
|
|
181
180
|
polling_interval_ms?: number; // Default: 1800000 (30 min)
|
|
182
181
|
last_sync?: string; // ISO timestamp
|
|
183
|
-
extraction_point?: string; // ISO timestamp -
|
|
182
|
+
extraction_point?: string; // ISO timestamp - cursor for single-session archive scan
|
|
183
|
+
processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
export interface CeremonyConfig {
|
|
@@ -238,7 +238,6 @@ export interface PersonaEntity {
|
|
|
238
238
|
last_activity: string;
|
|
239
239
|
last_heartbeat?: string;
|
|
240
240
|
last_extraction?: string;
|
|
241
|
-
last_inactivity_ping?: string;
|
|
242
241
|
}
|
|
243
242
|
|
|
244
243
|
export interface PersonaCreationInput {
|
|
@@ -257,6 +256,10 @@ export interface PersonaCreationInput {
|
|
|
257
256
|
export const MESSAGE_MIN_COUNT = 200;
|
|
258
257
|
export const MESSAGE_MAX_AGE_DAYS = 14;
|
|
259
258
|
|
|
259
|
+
// DLQ rolloff thresholds
|
|
260
|
+
export const DLQ_MAX_COUNT = 50;
|
|
261
|
+
export const DLQ_MAX_AGE_DAYS = 14;
|
|
262
|
+
|
|
260
263
|
// Reserved persona names (command keywords that conflict with /persona subcommands)
|
|
261
264
|
export const RESERVED_PERSONA_NAMES = ["new", "clone"] as const;
|
|
262
265
|
export type ReservedPersonaName = typeof RESERVED_PERSONA_NAMES[number];
|
|
@@ -272,14 +275,19 @@ export function isReservedPersonaName(name: string): boolean {
|
|
|
272
275
|
export interface Message {
|
|
273
276
|
id: string;
|
|
274
277
|
role: "human" | "system";
|
|
275
|
-
|
|
278
|
+
verbal_response?: string; // Human text or persona's spoken reply
|
|
279
|
+
action_response?: string; // Stage direction / action the persona performs
|
|
280
|
+
silence_reason?: string; // Why the persona chose not to respond (not shown to LLM)
|
|
276
281
|
timestamp: string;
|
|
277
|
-
read: boolean;
|
|
282
|
+
read: boolean; // Has human seen this system message?
|
|
278
283
|
context_status: ContextStatus;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
284
|
+
|
|
285
|
+
// Extraction completion flags (omit when false to save space)
|
|
286
|
+
// Single-letter names minimize storage overhead for large message histories
|
|
287
|
+
f?: boolean; // Fact extraction completed
|
|
288
|
+
r?: boolean; // tRait extraction completed
|
|
289
|
+
p?: boolean; // Person extraction completed
|
|
290
|
+
o?: boolean; // tOpic extraction completed
|
|
283
291
|
}
|
|
284
292
|
|
|
285
293
|
export interface ChatMessage {
|
|
@@ -291,12 +299,15 @@ export interface ChatMessage {
|
|
|
291
299
|
// LLM TYPES
|
|
292
300
|
// =============================================================================
|
|
293
301
|
|
|
302
|
+
export type LLMRequestState = "pending" | "processing" | "dlq";
|
|
303
|
+
|
|
294
304
|
export interface LLMRequest {
|
|
295
305
|
id: string;
|
|
296
306
|
created_at: string;
|
|
297
307
|
attempts: number;
|
|
298
308
|
last_attempt?: string;
|
|
299
309
|
retry_after?: string;
|
|
310
|
+
state: LLMRequestState;
|
|
300
311
|
type: LLMRequestType;
|
|
301
312
|
priority: LLMPriority;
|
|
302
313
|
system: string;
|
|
@@ -346,6 +357,7 @@ export interface MessageQueryOptions {
|
|
|
346
357
|
export interface QueueStatus {
|
|
347
358
|
state: "idle" | "busy" | "paused";
|
|
348
359
|
pending_count: number;
|
|
360
|
+
dlq_count: number;
|
|
349
361
|
current_operation?: string;
|
|
350
362
|
}
|
|
351
363
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { HumanEntity, PersonaEntity, Fact, Trait, Topic, Person, Quote, PersonaTopic } from "../types.ts";
|
|
2
|
+
export type CrossFindResult =
|
|
3
|
+
| { type: "fact" } & Fact
|
|
4
|
+
| { type: "trait" } & Trait
|
|
5
|
+
| { type: "topic" } & Topic
|
|
6
|
+
| { type: "person" } & Person
|
|
7
|
+
| { type: "quote" } & Quote
|
|
8
|
+
| { type: "persona" } & PersonaEntity
|
|
9
|
+
| { type: "personaTopic"; personaId: string } & PersonaTopic
|
|
10
|
+
| { type: "personaTrait"; personaId: string } & Trait;
|
|
11
|
+
|
|
12
|
+
export function crossFind(
|
|
13
|
+
id: string,
|
|
14
|
+
human: HumanEntity,
|
|
15
|
+
personas?: PersonaEntity[],
|
|
16
|
+
): CrossFindResult | null {
|
|
17
|
+
|
|
18
|
+
const fact = human.facts.find(f => f.id === id);
|
|
19
|
+
if (fact) return { type: "fact", ...fact };
|
|
20
|
+
|
|
21
|
+
const trait = human.traits.find(t => t.id === id);
|
|
22
|
+
if (trait) return { type: "trait", ...trait };
|
|
23
|
+
|
|
24
|
+
const person = human.people.find(p => p.id === id);
|
|
25
|
+
if (person) return { type: "person", ...person };
|
|
26
|
+
|
|
27
|
+
const topic = human.topics.find(t => t.id === id);
|
|
28
|
+
if (topic) return { type: "topic", ...topic };
|
|
29
|
+
|
|
30
|
+
const quote = human.quotes.find(q => q.id === id);
|
|
31
|
+
if (quote) return { type: "quote", ...quote };
|
|
32
|
+
|
|
33
|
+
for (const persona of personas ?? []) {
|
|
34
|
+
if (persona.id === id) return { type: "persona", ...persona };
|
|
35
|
+
|
|
36
|
+
const pTopic = persona.topics.find(t => t.id === id);
|
|
37
|
+
if (pTopic) return { type: "personaTopic", personaId: persona.id, ...pTopic };
|
|
38
|
+
|
|
39
|
+
const pTrait = persona.traits.find(t => t.id === id);
|
|
40
|
+
if (pTrait) return { type: "personaTrait", personaId: persona.id, ...pTrait };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|