ei-tui 0.6.0 → 0.6.1
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/core/handlers/human-matching.ts +12 -9
- package/src/prompts/human/topic-update.ts +1 -1
- package/tui/src/commands/me.tsx +12 -6
- package/tui/src/util/yaml-context.ts +66 -0
- package/tui/src/util/yaml-human.ts +274 -0
- package/tui/src/util/yaml-persona.ts +479 -0
- package/tui/src/util/yaml-provider.ts +215 -0
- package/tui/src/util/yaml-queue.ts +81 -0
- package/tui/src/util/yaml-quotes.ts +46 -0
- package/tui/src/util/yaml-serializers.ts +9 -1510
- package/tui/src/util/yaml-settings.ts +223 -0
- package/tui/src/util/yaml-shared.ts +32 -0
- package/tui/src/util/yaml-toolkit.ts +55 -0
package/package.json
CHANGED
|
@@ -123,10 +123,6 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
123
123
|
const roomId = response.request.data.roomId as string | undefined;
|
|
124
124
|
const candidateCategory = response.request.data.candidateCategory as string | undefined;
|
|
125
125
|
|
|
126
|
-
if (!result.name || !result.description || result.sentiment === undefined) {
|
|
127
|
-
throw new Error(`[handleTopicUpdate] Missing required fields: name=${result.name}, description=${!!result.description}, sentiment=${result.sentiment}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
126
|
const personaIds = personaId.split("|").filter(Boolean);
|
|
131
127
|
const primaryId = personaIds[0] ?? personaId;
|
|
132
128
|
|
|
@@ -147,14 +143,21 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
147
143
|
|
|
148
144
|
const existingTopic = isNewItem ? undefined : human.topics.find(t => t.id === existingItemId);
|
|
149
145
|
|
|
146
|
+
const resolvedName = result.name || existingTopic?.name;
|
|
147
|
+
const resolvedDescription = typeof result.description === 'string' ? result.description : existingTopic?.description;
|
|
148
|
+
|
|
149
|
+
if (!resolvedName || !resolvedDescription || result.sentiment === undefined) {
|
|
150
|
+
throw new Error(`[handleTopicUpdate] Missing required fields: name=${resolvedName}, description=${!!resolvedDescription}, sentiment=${result.sentiment}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
150
153
|
let embedding: number[] | undefined;
|
|
151
154
|
try {
|
|
152
155
|
const embeddingService = getEmbeddingService();
|
|
153
156
|
const category = result.category ?? candidateCategory ?? existingTopic?.category;
|
|
154
|
-
const text = getTopicEmbeddingText({ name:
|
|
157
|
+
const text = getTopicEmbeddingText({ name: resolvedName, category, description: resolvedDescription });
|
|
155
158
|
embedding = await embeddingService.embed(text);
|
|
156
159
|
} catch (err) {
|
|
157
|
-
console.warn(`[handleTopicUpdate] Failed to compute embedding for topic "${
|
|
160
|
+
console.warn(`[handleTopicUpdate] Failed to compute embedding for topic "${resolvedName}":`, err);
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
const exposureImpact = result.exposure_impact as ExposureImpact | undefined;
|
|
@@ -167,8 +170,8 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
167
170
|
|
|
168
171
|
const topic: Topic = {
|
|
169
172
|
id: itemId,
|
|
170
|
-
name:
|
|
171
|
-
description:
|
|
173
|
+
name: resolvedName,
|
|
174
|
+
description: resolvedDescription,
|
|
172
175
|
sentiment: result.sentiment,
|
|
173
176
|
category: result.category ?? candidateCategory ?? existingTopic?.category,
|
|
174
177
|
exposure_current: calculateExposureCurrent(exposureImpact, existingTopic?.exposure_current ?? 0),
|
|
@@ -189,7 +192,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
189
192
|
: state.messages_get(personaId);
|
|
190
193
|
await validateAndStoreQuotes(result.quotes, allMessages, itemId, personaDisplayName, personaGroup, state);
|
|
191
194
|
|
|
192
|
-
console.log(`[handleTopicUpdate] ${isNewItem ? "Created" : "Updated"} topic "${
|
|
195
|
+
console.log(`[handleTopicUpdate] ${isNewItem ? "Created" : "Updated"} topic "${resolvedName}"`);
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
export async function handlePersonUpdate(response: LLMResponse, state: StateManager): Promise<void> {
|
|
@@ -250,7 +250,7 @@ ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided
|
|
|
250
250
|
${jsonTemplate}
|
|
251
251
|
\`\`\`
|
|
252
252
|
|
|
253
|
-
When returning a record,
|
|
253
|
+
When returning a record, always include \`sentiment\`. Include \`name\` only if you are changing it; omit it to keep the existing name. Always include \`description\` when returning a record.
|
|
254
254
|
|
|
255
255
|
If you find **NO EVIDENCE** of this TOPIC in the "Most Recent Messages", respond with: \`{}\`
|
|
256
256
|
|
package/tui/src/commands/me.tsx
CHANGED
|
@@ -80,21 +80,27 @@ export const meCommand: Command = {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
for (const fact of parsed.facts) {
|
|
83
|
-
|
|
83
|
+
if (parsed.changedFactIds.has(fact.id)) {
|
|
84
|
+
await ctx.ei.upsertFact(fact);
|
|
85
|
+
}
|
|
84
86
|
}
|
|
85
87
|
for (const topic of parsed.topics) {
|
|
86
|
-
|
|
88
|
+
if (parsed.changedTopicIds.has(topic.id)) {
|
|
89
|
+
await ctx.ei.upsertTopic(topic);
|
|
90
|
+
}
|
|
87
91
|
}
|
|
88
92
|
for (const person of parsed.people) {
|
|
89
|
-
|
|
93
|
+
if (parsed.changedPersonIds.has(person.id)) {
|
|
94
|
+
await ctx.ei.upsertPerson(person);
|
|
95
|
+
}
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
const deleteCount = parsed.deletedFactIds.length +
|
|
93
99
|
parsed.deletedTopicIds.length +
|
|
94
100
|
parsed.deletedPersonIds.length;
|
|
95
|
-
const updateCount = parsed.
|
|
96
|
-
parsed.
|
|
97
|
-
parsed.
|
|
101
|
+
const updateCount = parsed.changedFactIds.size +
|
|
102
|
+
parsed.changedTopicIds.size +
|
|
103
|
+
parsed.changedPersonIds.size;
|
|
98
104
|
|
|
99
105
|
ctx.showNotification(`Updated ${updateCount} items, deleted ${deleteCount}`, "info");
|
|
100
106
|
return;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
import type { Message } from "../../../src/core/types.js";
|
|
3
|
+
import { ContextStatus } from "../../../src/core/types.js";
|
|
4
|
+
|
|
5
|
+
interface EditableMessage {
|
|
6
|
+
id: string;
|
|
7
|
+
role: "human" | "system";
|
|
8
|
+
timestamp: string;
|
|
9
|
+
context_status: ContextStatus;
|
|
10
|
+
_delete?: boolean;
|
|
11
|
+
content?: string;
|
|
12
|
+
silence_reason?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getContent(m: { content?: string; verbal_response?: string; action_response?: string }): string {
|
|
16
|
+
if (m.content) return m.content;
|
|
17
|
+
const parts: string[] = [];
|
|
18
|
+
if (m.action_response) parts.push(`_${m.action_response}_`);
|
|
19
|
+
if (m.verbal_response) parts.push(m.verbal_response);
|
|
20
|
+
return parts.join('\n\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function contextToYAML(messages: Message[]): string {
|
|
24
|
+
const header = [
|
|
25
|
+
"# context_status: default | always | never",
|
|
26
|
+
"# _delete: true — permanently removes the message",
|
|
27
|
+
"# content | silence_reason",
|
|
28
|
+
].join("\n");
|
|
29
|
+
|
|
30
|
+
const data: EditableMessage[] = messages.map((m) => ({
|
|
31
|
+
id: m.id,
|
|
32
|
+
role: m.role,
|
|
33
|
+
timestamp: m.timestamp,
|
|
34
|
+
context_status: m.context_status,
|
|
35
|
+
_delete: false,
|
|
36
|
+
content: getContent(m) || undefined,
|
|
37
|
+
silence_reason: m.silence_reason,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
return header + "\n" + YAML.stringify(data, { lineWidth: 0 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ContextYAMLResult {
|
|
44
|
+
messages: Array<{ id: string; context_status: ContextStatus }>;
|
|
45
|
+
deletedMessageIds: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function contextFromYAML(yamlContent: string): ContextYAMLResult {
|
|
49
|
+
const data = YAML.parse(yamlContent) as EditableMessage[];
|
|
50
|
+
|
|
51
|
+
const deletedMessageIds: string[] = [];
|
|
52
|
+
const messages: Array<{ id: string; context_status: ContextStatus }> = [];
|
|
53
|
+
|
|
54
|
+
for (const m of data ?? []) {
|
|
55
|
+
if (m._delete) {
|
|
56
|
+
deletedMessageIds.push(m.id);
|
|
57
|
+
} else {
|
|
58
|
+
const normalized = (m.context_status ?? 'default').toString().toLowerCase() as ContextStatus;
|
|
59
|
+
messages.push({ id: m.id, context_status: normalized });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { messages, deletedMessageIds };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
import type {
|
|
3
|
+
HumanEntity,
|
|
4
|
+
Fact,
|
|
5
|
+
Topic,
|
|
6
|
+
Person,
|
|
7
|
+
PersonIdentifier,
|
|
8
|
+
} from "../../../src/core/types.js";
|
|
9
|
+
import { BUILT_IN_FACT_NAMES } from "../../../src/core/constants/built-in-facts.js";
|
|
10
|
+
import { BUILT_IN_IDENTIFIER_TYPES } from "../../../src/core/constants/built-in-identifier-types.js";
|
|
11
|
+
|
|
12
|
+
interface EditableTopic extends Omit<Topic, 'persona_groups'> {
|
|
13
|
+
_delete?: boolean;
|
|
14
|
+
persona_groups?: Record<string, boolean>[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface EditableFact extends Omit<Fact, 'persona_groups'> {
|
|
18
|
+
_delete?: boolean;
|
|
19
|
+
persona_groups?: Record<string, boolean>[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface YAMLPersonIdentifier {
|
|
23
|
+
type: string;
|
|
24
|
+
value: string;
|
|
25
|
+
primary?: true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface EditablePersonYAML extends Omit<Person, 'identifiers' | 'persona_groups'> {
|
|
29
|
+
identifiers: YAMLPersonIdentifier[];
|
|
30
|
+
_delete?: boolean;
|
|
31
|
+
persona_groups?: Record<string, boolean>[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface EditableHumanData {
|
|
35
|
+
facts: EditableFact[];
|
|
36
|
+
topics: EditableTopic[];
|
|
37
|
+
people: EditablePersonYAML[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type WithReadOnlyFields = {
|
|
41
|
+
learned_on?: string;
|
|
42
|
+
learned_by?: string;
|
|
43
|
+
validated_date?: string;
|
|
44
|
+
last_mentioned?: string;
|
|
45
|
+
last_updated: string;
|
|
46
|
+
last_changed_by?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function readOnlyToEnd<T extends WithReadOnlyFields>(item: T): T {
|
|
50
|
+
const { learned_on, learned_by, validated_date, last_mentioned, last_updated, last_changed_by, ...rest } = item;
|
|
51
|
+
return { ...rest, learned_on, learned_by, validated_date, last_mentioned, last_updated, last_changed_by } as T;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildGroupCheckboxMap(itemGroups: string[], allGroups: string[]): Record<string, boolean>[] {
|
|
55
|
+
const activeSet = new Set(itemGroups);
|
|
56
|
+
return [...new Set([...allGroups, ...itemGroups])].map(g => ({ [g]: activeSet.has(g) }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toYAMLIdentifiers(identifiers: PersonIdentifier[], personaLookup?: Map<string, string>): YAMLPersonIdentifier[] {
|
|
60
|
+
return identifiers.map(({ type, value, is_primary }) => {
|
|
61
|
+
const resolvedValue = type === 'Ei Persona' ? (personaLookup?.get(value) ?? value) : value;
|
|
62
|
+
const entry: YAMLPersonIdentifier = { type, value: resolvedValue };
|
|
63
|
+
if (is_primary) entry.primary = true;
|
|
64
|
+
return entry;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function knownTypesComment(personaLookup?: Map<string, string>): string {
|
|
69
|
+
const lines = [`# Valid types: ${BUILT_IN_IDENTIFIER_TYPES.join(', ')}`];
|
|
70
|
+
if (personaLookup && personaLookup.size > 0) {
|
|
71
|
+
lines.push(`# Personas: ${Array.from(personaLookup.values()).join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseGroupCheckboxMap(groups: Record<string, boolean>[] | undefined): string[] {
|
|
77
|
+
if (!groups) return [];
|
|
78
|
+
const result: string[] = [];
|
|
79
|
+
for (const record of groups) {
|
|
80
|
+
for (const [name, active] of Object.entries(record)) {
|
|
81
|
+
if (active) result.push(name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function humanToYAML(human: HumanEntity, personaLookup?: Map<string, string>, allGroups: string[] = []): string {
|
|
88
|
+
const data: EditableHumanData = {
|
|
89
|
+
facts: human.facts.map(f => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(f); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
|
|
90
|
+
topics: human.topics.map(t => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(t); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
|
|
91
|
+
people: human.people.map(p => {
|
|
92
|
+
const { identifiers, interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(p);
|
|
93
|
+
return {
|
|
94
|
+
...rest,
|
|
95
|
+
persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups),
|
|
96
|
+
identifiers: toYAMLIdentifiers(identifiers ?? [], personaLookup),
|
|
97
|
+
_delete: false as const,
|
|
98
|
+
};
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const personComment = knownTypesComment(personaLookup);
|
|
103
|
+
|
|
104
|
+
return YAML.stringify(data, {
|
|
105
|
+
lineWidth: 0,
|
|
106
|
+
})
|
|
107
|
+
.replace(/^(\s+)(learned_on: .+)$/mg, '$1# [read-only] $2')
|
|
108
|
+
.replace(/^(\s+)(learned_by: )(.+)$/mg, (_, indent, key, val) => {
|
|
109
|
+
const trimmed = val.trim();
|
|
110
|
+
const displayName = personaLookup?.get(trimmed) ?? trimmed;
|
|
111
|
+
return `${indent}# [read-only] ${key}${displayName}`;
|
|
112
|
+
})
|
|
113
|
+
.replace(/^(\s+)(validated_date: .+)$/mg, '$1# [read-only] $2')
|
|
114
|
+
.replace(/^(\s+)(last_mentioned: .+)$/mg, '$1# [read-only] $2')
|
|
115
|
+
.replace(/^(\s+)(last_updated: .+)$/mg, '$1# [read-only] $2')
|
|
116
|
+
.replace(/^(\s+)(last_changed_by: )(.+)$/mg, (_, indent, key, val) => {
|
|
117
|
+
const trimmed = val.trim();
|
|
118
|
+
const displayName = personaLookup?.get(trimmed) ?? trimmed;
|
|
119
|
+
return `${indent}# [read-only] ${key}${displayName}`;
|
|
120
|
+
})
|
|
121
|
+
.replace(/^(\s+)(identifiers:)/mg, (_, indent, _key) => {
|
|
122
|
+
return `${indent}${personComment}\n${indent}identifiers:`;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface HumanYAMLResult {
|
|
127
|
+
facts: Fact[];
|
|
128
|
+
topics: Topic[];
|
|
129
|
+
people: Person[];
|
|
130
|
+
deletedFactIds: string[];
|
|
131
|
+
deletedTopicIds: string[];
|
|
132
|
+
deletedPersonIds: string[];
|
|
133
|
+
changedFactIds: Set<string>;
|
|
134
|
+
changedTopicIds: Set<string>;
|
|
135
|
+
changedPersonIds: Set<string>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function identifiersEqual(a: PersonIdentifier[] | undefined, b: PersonIdentifier[] | undefined): boolean {
|
|
139
|
+
const normalize = (ids: PersonIdentifier[] | undefined) =>
|
|
140
|
+
[...(ids ?? [])].sort((x, y) => `${x.type}:${x.value}`.localeCompare(`${y.type}:${y.value}`));
|
|
141
|
+
const na = normalize(a);
|
|
142
|
+
const nb = normalize(b);
|
|
143
|
+
if (na.length !== nb.length) return false;
|
|
144
|
+
return na.every((id, i) =>
|
|
145
|
+
id.type === nb[i].type &&
|
|
146
|
+
id.value === nb[i].value &&
|
|
147
|
+
Boolean(id.is_primary) === Boolean(nb[i].is_primary)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function groupsEqual(a: string[] | undefined, b: string[] | undefined): boolean {
|
|
152
|
+
const sa = [...(a ?? [])].sort();
|
|
153
|
+
const sb = [...(b ?? [])].sort();
|
|
154
|
+
if (sa.length !== sb.length) return false;
|
|
155
|
+
return sa.every((v, i) => v === sb[i]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function factChanged(parsed: Fact, original: Fact): boolean {
|
|
159
|
+
const scalarFields: (keyof Fact)[] = [
|
|
160
|
+
'name', 'description', 'sentiment',
|
|
161
|
+
];
|
|
162
|
+
for (const field of scalarFields) {
|
|
163
|
+
if (parsed[field] !== original[field]) return true;
|
|
164
|
+
}
|
|
165
|
+
return !groupsEqual(parsed.persona_groups, original.persona_groups);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function topicChanged(parsed: Topic, original: Topic): boolean {
|
|
169
|
+
const scalarFields: (keyof Topic)[] = [
|
|
170
|
+
'name', 'description', 'sentiment', 'exposure_current', 'exposure_desired', 'category',
|
|
171
|
+
];
|
|
172
|
+
for (const field of scalarFields) {
|
|
173
|
+
if (parsed[field] !== original[field]) return true;
|
|
174
|
+
}
|
|
175
|
+
return !groupsEqual(parsed.persona_groups, original.persona_groups);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function personChanged(parsed: Person, original: Person): boolean {
|
|
179
|
+
const scalarFields: (keyof Person)[] = [
|
|
180
|
+
'name', 'description', 'sentiment', 'relationship',
|
|
181
|
+
'exposure_current', 'exposure_desired',
|
|
182
|
+
];
|
|
183
|
+
for (const field of scalarFields) {
|
|
184
|
+
if (parsed[field] !== original[field]) return true;
|
|
185
|
+
}
|
|
186
|
+
if (!groupsEqual(parsed.persona_groups, original.persona_groups)) return true;
|
|
187
|
+
return !identifiersEqual(parsed.identifiers, original.identifiers);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function humanFromYAML(yamlContent: string, original?: HumanEntity): HumanYAMLResult {
|
|
191
|
+
const stripped = yamlContent
|
|
192
|
+
.split('\n')
|
|
193
|
+
.filter(line => !/^\s*#\s*\[read-only\]/.test(line))
|
|
194
|
+
.join('\n');
|
|
195
|
+
const data = YAML.parse(stripped) as EditableHumanData;
|
|
196
|
+
|
|
197
|
+
const deletedFactIds: string[] = [];
|
|
198
|
+
const deletedTopicIds: string[] = [];
|
|
199
|
+
const deletedPersonIds: string[] = [];
|
|
200
|
+
const changedFactIds = new Set<string>();
|
|
201
|
+
const changedTopicIds = new Set<string>();
|
|
202
|
+
const changedPersonIds = new Set<string>();
|
|
203
|
+
|
|
204
|
+
const facts: Fact[] = [];
|
|
205
|
+
for (const f of data.facts ?? []) {
|
|
206
|
+
if (f._delete && !BUILT_IN_FACT_NAMES.has(f.name)) {
|
|
207
|
+
deletedFactIds.push(f.id);
|
|
208
|
+
} else {
|
|
209
|
+
const { _delete, persona_groups: groupMap, ...parsed } = f;
|
|
210
|
+
const originalFact = original?.facts.find(of => of.id === parsed.id);
|
|
211
|
+
const fact: Fact = originalFact
|
|
212
|
+
? { ...originalFact, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
213
|
+
: { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
214
|
+
facts.push(fact);
|
|
215
|
+
if (!originalFact || factChanged(fact, originalFact)) {
|
|
216
|
+
if (fact.description && !originalFact?.validated_date) {
|
|
217
|
+
fact.validated_date = new Date().toISOString();
|
|
218
|
+
}
|
|
219
|
+
changedFactIds.add(fact.id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const topics: Topic[] = [];
|
|
225
|
+
for (const t of data.topics ?? []) {
|
|
226
|
+
if (t._delete) {
|
|
227
|
+
deletedTopicIds.push(t.id);
|
|
228
|
+
} else {
|
|
229
|
+
const { _delete, persona_groups: groupMap, ...parsed } = t;
|
|
230
|
+
const originalTopic = original?.topics.find(ot => ot.id === parsed.id);
|
|
231
|
+
const topic: Topic = originalTopic
|
|
232
|
+
? { ...originalTopic, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
233
|
+
: { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
234
|
+
topics.push(topic);
|
|
235
|
+
if (!originalTopic || topicChanged(topic, originalTopic)) {
|
|
236
|
+
changedTopicIds.add(topic.id);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const people: Person[] = [];
|
|
242
|
+
for (const p of data.people ?? []) {
|
|
243
|
+
if (p._delete) {
|
|
244
|
+
deletedPersonIds.push(p.id);
|
|
245
|
+
} else {
|
|
246
|
+
const { _delete, identifiers: yamlIdentifiers, persona_groups: groupMap, ...parsed } = p;
|
|
247
|
+
const identifiers: PersonIdentifier[] = (yamlIdentifiers ?? []).map(({ type, value, primary }) => ({
|
|
248
|
+
type,
|
|
249
|
+
value,
|
|
250
|
+
...(primary ? { is_primary: true } : {}),
|
|
251
|
+
}));
|
|
252
|
+
const originalPerson = original?.people.find(op => op.id === parsed.id);
|
|
253
|
+
const person: Person = originalPerson
|
|
254
|
+
? { ...originalPerson, ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
255
|
+
: { ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
256
|
+
people.push(person);
|
|
257
|
+
if (!originalPerson || personChanged(person, originalPerson)) {
|
|
258
|
+
changedPersonIds.add(person.id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
facts,
|
|
265
|
+
topics,
|
|
266
|
+
people,
|
|
267
|
+
deletedFactIds,
|
|
268
|
+
deletedTopicIds,
|
|
269
|
+
deletedPersonIds,
|
|
270
|
+
changedFactIds,
|
|
271
|
+
changedTopicIds,
|
|
272
|
+
changedPersonIds,
|
|
273
|
+
};
|
|
274
|
+
}
|