Skip to content

Commit aa0d064

Browse files
authored
Merge pull request #64 from Rhystic1/integrated
Story/Roleplaying Generator - Update #1
2 parents 5a464f6 + 2ef2173 commit aa0d064

File tree

11 files changed

+3785
-993
lines changed

11 files changed

+3785
-993
lines changed

src/components/views/ChatInterface.vue

Lines changed: 1313 additions & 991 deletions
Large diffs are not rendered by default.

src/utils/aiActionNormalization.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
export type AiAction = Record<string, any>
2+
3+
const normalizeKey = (name: string): string => {
4+
return (name || '')
5+
.toLowerCase()
6+
.trim()
7+
.replace(/[^a-z0-9]+/g, ' ')
8+
.replace(/\s+/g, ' ')
9+
}
10+
11+
const resolveExistingProfileKey = (name: string, existingKeys: string[]): string | undefined => {
12+
if (!name) return undefined
13+
14+
// 1) Case-insensitive exact match
15+
const exact = existingKeys.find((k) => k.toLowerCase() === name.toLowerCase())
16+
if (exact) return exact
17+
18+
// 2) Normalized match, but ONLY if unique
19+
const needle = normalizeKey(name)
20+
if (!needle) return undefined
21+
22+
const matches = existingKeys.filter((k) => normalizeKey(k) === needle)
23+
if (matches.length === 1) return matches[0]
24+
25+
return undefined
26+
}
27+
28+
const looksLikeCharacterRecord = (obj: any): obj is Record<string, any> => {
29+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false
30+
31+
// If it's a direct profile object (personality/speech_style/relationships), it's NOT a record.
32+
const directProfileKeys = new Set(['personality', 'speech_style', 'relationships', 'id', 'name'])
33+
const keys = Object.keys(obj)
34+
if (keys.length > 0 && keys.every((k) => directProfileKeys.has(k))) return false
35+
36+
// Heuristic: a record has at least one value that is an object
37+
return keys.some((k) => typeof (obj as any)[k] === 'object' && (obj as any)[k] !== null)
38+
}
39+
40+
const mergeProgressionUpdate = (target: any, incoming: any) => {
41+
if (!incoming || typeof incoming !== 'object') return
42+
43+
if (incoming.personality) {
44+
target.personality = incoming.personality
45+
}
46+
47+
if (incoming.relationships && typeof incoming.relationships === 'object') {
48+
target.relationships = {
49+
...(target.relationships || {}),
50+
...incoming.relationships
51+
}
52+
}
53+
}
54+
55+
/**
56+
* Normalizes an AI action so that:
57+
* - Any legacy/hallucinated `characterProfile` / `characterProfiles` blocks are NOT allowed to overwrite existing profiles.
58+
* - If a legacy/memory block targets an existing character, we extract allowed progression updates
59+
* (personality + relationships) and move them into `characterProgression`.
60+
* - Unsafe fields (e.g. `speech_style`) are silently ignored (they will also be blocked later in executeAction).
61+
*/
62+
export const normalizeAiActionCharacterData = (
63+
action: AiAction,
64+
knownCharacterProfiles: Record<string, any>
65+
): AiAction => {
66+
const out: AiAction = { ...(action || {}) }
67+
const existingKeys = Object.keys(knownCharacterProfiles || {})
68+
69+
// 1) Fold legacy keys into memory (but we'll re-route existing characters into characterProgression)
70+
const legacyBlocks: any[] = []
71+
if (out.characterProfile) legacyBlocks.push(out.characterProfile)
72+
if (out.characterProfiles) legacyBlocks.push(out.characterProfiles)
73+
74+
if (legacyBlocks.length > 0) {
75+
delete out.characterProfile
76+
delete out.characterProfiles
77+
78+
for (const legacy of legacyBlocks) {
79+
if (!looksLikeCharacterRecord(legacy)) continue
80+
out.memory = {
81+
...(out.memory && typeof out.memory === 'object' ? out.memory : {}),
82+
...legacy
83+
}
84+
}
85+
}
86+
87+
// 2) If memory targets an existing character, convert to characterProgression.
88+
if (out.memory && typeof out.memory === 'object' && !Array.isArray(out.memory)) {
89+
const newMemory: Record<string, any> = {}
90+
91+
for (const [rawName, profileLike] of Object.entries(out.memory)) {
92+
const resolvedExistingKey = resolveExistingProfileKey(rawName, existingKeys)
93+
94+
if (resolvedExistingKey && profileLike && typeof profileLike === 'object' && !Array.isArray(profileLike)) {
95+
out.characterProgression = {
96+
...(out.characterProgression && typeof out.characterProgression === 'object' ? out.characterProgression : {})
97+
}
98+
99+
const current =
100+
(out.characterProgression as any)[resolvedExistingKey] &&
101+
typeof (out.characterProgression as any)[resolvedExistingKey] === 'object'
102+
? (out.characterProgression as any)[resolvedExistingKey]
103+
: {}
104+
105+
mergeProgressionUpdate(current, profileLike)
106+
;(out.characterProgression as any)[resolvedExistingKey] = current
107+
108+
// Do NOT keep this entry in memory (prevents profile override/duplication)
109+
continue
110+
}
111+
112+
newMemory[rawName] = profileLike
113+
}
114+
115+
out.memory = newMemory
116+
if (Object.keys(out.memory).length === 0) delete out.memory
117+
}
118+
119+
return out
120+
}

0 commit comments

Comments
 (0)