192192 <n-select v-model:value =" tokenUsage" :options =" tokenUsageOptions" />
193193 </n-form-item >
194194
195- <n-form-item >
195+ <n-form-item v-if = " isDev " >
196196 <template #label >
197197 Context Caching <span style =" font-size : smaller ;" >(Experimental)</span >
198198 <n-popover trigger =" hover" placement =" bottom" >
@@ -377,7 +377,7 @@ import loadingMessages from '@/utils/json/loadingMessages.json'
377377import prompts from ' @/utils/json/prompts.json'
378378import { marked } from ' marked'
379379import { animationMappings } from ' @/utils/animationMappings'
380- import { cleanWikiContent , sanitizeActions , splitNarration , parseFallback } from ' @/utils/chatUtils'
380+ import { cleanWikiContent , sanitizeActions , splitNarration , parseFallback , parseAIResponse , isWholeWordPresent } from ' @/utils/chatUtils'
381381
382382// Helper to get honorific with fallback to "Commander"
383383const getHonorific = (characterName : string ): string => {
@@ -403,7 +403,7 @@ const apiKey = ref(localStorage.getItem('nikke_api_key') || '')
403403const model = ref (' sonar' )
404404const mode = ref (' roleplay' )
405405const tokenUsage = ref (' medium' )
406- const enableContextCaching = ref (false )
406+ const enableContextCaching = ref (true )
407407const playbackMode = ref (' auto' )
408408const ttsEnabled = ref (false )
409409const ttsEndpoint = ref (' http://localhost:7851' )
@@ -431,6 +431,7 @@ const openRouterModels = ref<any[]>([])
431431const isRestoring = ref (false )
432432const needsJsonReminder = ref (false )
433433const modelsWithoutJsonSupport = new Set <string >() // Track models that don't support json_object
434+ const isDev = import .meta .env .DEV
434435
435436// Helper to set random loading message
436437const setRandomLoadingMessage = () => {
@@ -1118,10 +1119,10 @@ const callAI = async (isRetry: boolean = false): Promise<string> => {
11181119 // If using local profiles, pre-load profiles for any characters mentioned in the first prompt
11191120 if (isFirstTurn && useLocalProfiles .value && chatHistory .value .length > 0 ) {
11201121 const firstPrompt = chatHistory .value [chatHistory .value .length - 1 ].content
1121- // Simple heuristic: check if any known local character name appears in the prompt
1122+ // Use whole word matching to avoid substring matches (e.g. "Crow" from "Crown")
11221123 const localNames = Object .keys (localCharacterProfiles )
11231124 const foundNames = localNames .filter (name =>
1124- firstPrompt . toLowerCase (). includes ( name . toLowerCase () )
1125+ isWholeWordPresent ( firstPrompt , name )
11251126 )
11261127
11271128 if (foundNames .length > 0 ) {
@@ -1290,7 +1291,7 @@ const callAI = async (isRetry: boolean = false): Promise<string> => {
12901291 }
12911292
12921293 // Check if the model needs to search for new characters
1293- const searchRequest = await checkForSearchRequest (response )
1294+ const searchRequest = await checkForSearchRequest (response , lastPrompt . value )
12941295 if (searchRequest && searchRequest .length > 0 ) {
12951296 logDebug (' [callAI] Model requested search for characters:' , searchRequest )
12961297 // Perform search for unknown characters
@@ -1402,7 +1403,7 @@ const callAIWithoutSearch = async (isRetry: boolean = false): Promise<string> =>
14021403}
14031404
14041405// Check if the AI response contains a search request for unknown characters
1405- const checkForSearchRequest = async (response : string ): Promise <string [] | null > => {
1406+ const checkForSearchRequest = async (response : string , userPrompt : string = ' ' ): Promise <string [] | null > => {
14061407 try {
14071408 let jsonStr = response .replace (/ ```json\n ? | \n ? ```/ g , ' ' ).trim ()
14081409 const start = jsonStr .indexOf (' [' )
@@ -1421,12 +1422,24 @@ const checkForSearchRequest = async (response: string): Promise<string[] | null>
14211422 // Look for needs_search field in any action
14221423 for (const action of data ) {
14231424 if (action .needs_search && Array .isArray (action .needs_search ) && action .needs_search .length > 0 ) {
1424- // Filter out characters we already have profiles for
1425- const unknownChars = action .needs_search .filter (
1426- (name : string ) => ! characterProfiles .value [name ]
1425+ // Validate characters using whole-word matching against user prompt and AI text
1426+ const allGeneratedText = data .map ((a : any ) => a .text || ' ' ).join (' ' )
1427+
1428+ const validatedChars = action .needs_search .filter (
1429+ (name : string ) => {
1430+ // Skip if already known or is "Commander"
1431+ if (characterProfiles .value [name ] || name .toLowerCase () === ' commander' ) return false
1432+
1433+ // Character must appear as whole word in either user prompt or AI response
1434+ const inUserPrompt = userPrompt && isWholeWordPresent (userPrompt , name )
1435+ const inGeneratedText = allGeneratedText && isWholeWordPresent (allGeneratedText , name )
1436+
1437+ return inUserPrompt || inGeneratedText
1438+ }
14271439 )
1428- if (unknownChars .length > 0 ) {
1429- return unknownChars
1440+
1441+ if (validatedChars .length > 0 ) {
1442+ return validatedChars
14301443 }
14311444 }
14321445 }
@@ -2198,85 +2211,7 @@ const processAIResponse = async (responseStr: string) => {
21982211 let data: any [] = []
21992212
22002213 try {
2201- let jsonStr = responseStr
2202-
2203- // FIRST: Try to extract JSON from markdown code blocks (highest priority)
2204- const jsonBlockMatch = responseStr .match (/ ```(?:json)? \s * ([\s\S ] *? )```/ )
2205- if (jsonBlockMatch ) {
2206- jsonStr = jsonBlockMatch [1 ].trim ()
2207- } else {
2208- // Remove any stray markdown markers
2209- jsonStr = responseStr .replace (/ ```json\n ? | \n ? ```/ g , ' ' ).trim ()
2210- }
2211-
2212- // Attempt to extract JSON structure (array or object) if mixed with text
2213- const firstOpenBrace = jsonStr .indexOf (' {' )
2214- const firstOpenBracket = jsonStr .indexOf (' [' )
2215-
2216- let start = - 1
2217- let end = - 1
2218-
2219- // Determine if we should look for an array or an object based on which appears first
2220- if (firstOpenBracket !== - 1 && (firstOpenBrace === - 1 || firstOpenBracket < firstOpenBrace )) {
2221- start = firstOpenBracket
2222- end = jsonStr .lastIndexOf (' ]' )
2223- } else if (firstOpenBrace !== - 1 ) {
2224- start = firstOpenBrace
2225- end = jsonStr .lastIndexOf (' }' )
2226- }
2227-
2228- if (start !== - 1 && end !== - 1 ) {
2229- jsonStr = jsonStr .substring (start , end + 1 )
2230- }
2231-
2232- // Try to repair common JSON errors
2233- const tryParseJSON = (str : string ): any => {
2234- // First attempt: parse as-is
2235- try {
2236- return JSON .parse (str )
2237- } catch (e ) {
2238- // Repair attempt: fix unbalanced braces/brackets
2239- let repaired = str
2240-
2241- // Count braces and brackets
2242- const openBraces = (repaired .match (/ {/ g ) || []).length
2243- const closeBraces = (repaired .match (/ }/ g ) || []).length
2244- const openBrackets = (repaired .match (/ \[ / g ) || []).length
2245- const closeBrackets = (repaired .match (/ ]/ g ) || []).length
2246-
2247- // Add missing closing braces
2248- if (openBraces > closeBraces ) {
2249- repaired += ' }' .repeat (openBraces - closeBraces )
2250- }
2251- // Add missing closing brackets
2252- if (openBrackets > closeBrackets ) {
2253- repaired += ' ]' .repeat (openBrackets - closeBrackets )
2254- }
2255-
2256- // Try again with repaired string
2257- try {
2258- return JSON .parse (repaired )
2259- } catch (e2 ) {
2260- // If still failing, try removing the memory object entirely (it's optional)
2261- const withoutMemory = str .replace (/ "memory"\s * :\s * \{ [^ }] * (\{ [^ }] * \} [^ }] * )* \} \s * ,? / g , ' ' )
2262- try {
2263- return JSON .parse (withoutMemory )
2264- } catch (e3 ) {
2265- throw e // Throw original error
2266- }
2267- }
2268- }
2269- }
2270-
2271- data = tryParseJSON (jsonStr )
2272-
2273- if (! Array .isArray (data )) {
2274- data = [data ]
2275- }
2276-
2277- // Sanitize and split actions to ensure narration/dialogue separation
2278- data = sanitizeActions (data )
2279-
2214+ data = parseAIResponse (responseStr )
22802215 } catch (e ) {
22812216 console .warn (' JSON parse failed, attempting text fallback parsing...' , e )
22822217
@@ -2534,10 +2469,16 @@ const executeAction = async (data: any) => {
25342469 }
25352470
25362471 if (data .memory ) {
2537- logDebug (' [AI Memory Update]:' , data .memory )
2538- // Filter out any honorific fields and Commander from relationships - we use the static honorifics.json for those
2539- const filteredMemory : Record < string , any > = {}
2472+ logDebug (' [AI Memory Update - New Characters Only ]:' , data .memory )
2473+ const newProfiles : Record < string , any > = {}
2474+
25402475 for (const [charName, profile] of Object .entries (data .memory )) {
2476+ // Skip if character already exists in profiles
2477+ if (characterProfiles .value [charName ]) {
2478+ logDebug (` [AI Memory] Skipping existing character '${charName }' in memory block. Use characterProgression to update. ` )
2479+ continue
2480+ }
2481+
25412482 if (typeof profile === ' object' && profile !== null ) {
25422483 const { honorific_for_commander, honorific_to_commander, honorific, relationships, ... rest } = profile as any
25432484
@@ -2548,15 +2489,62 @@ const executeAction = async (data: any) => {
25482489 filteredRelationships = Object .keys (otherRelationships ).length > 0 ? otherRelationships : undefined
25492490 }
25502491
2551- filteredMemory [charName ] = {
2492+ newProfiles [charName ] = {
25522493 ... rest ,
25532494 ... (filteredRelationships && { relationships: filteredRelationships })
25542495 }
25552496 } else {
2556- filteredMemory [charName ] = profile
2497+ newProfiles [charName ] = profile
25572498 }
25582499 }
2559- characterProfiles .value = { ... characterProfiles .value , ... filteredMemory }
2500+
2501+ if (Object .keys (newProfiles ).length > 0 ) {
2502+ characterProfiles .value = { ... characterProfiles .value , ... newProfiles }
2503+ }
2504+ }
2505+
2506+ // Handle 'characterProgression' - For EXISTING characters (Personality/Relationships ONLY)
2507+ if (data .characterProgression ) {
2508+ logDebug (' [AI Character Progression]:' , data .characterProgression )
2509+ const updatedProfiles = { ... characterProfiles .value }
2510+ let hasUpdates = false
2511+
2512+ for (const [charName, progression] of Object .entries (data .characterProgression )) {
2513+ if (updatedProfiles [charName ] && typeof progression === ' object' && progression !== null ) {
2514+ const currentProfile = updatedProfiles [charName ]
2515+ const updates = progression as any
2516+
2517+ // 1. Update Personality if provided
2518+ if (updates .personality ) {
2519+ currentProfile .personality = updates .personality
2520+ hasUpdates = true
2521+ }
2522+
2523+ // 2. Update Relationships if provided
2524+ if (updates .relationships && typeof updates .relationships === ' object' ) {
2525+ // Filter Commander out
2526+ const { Commander, commander, ... otherRelationships } = updates .relationships
2527+ const validRelationships = Object .keys (otherRelationships ).length > 0 ? otherRelationships : {}
2528+
2529+ currentProfile .relationships = {
2530+ ... (currentProfile .relationships || {}),
2531+ ... validRelationships
2532+ }
2533+ hasUpdates = true
2534+ }
2535+
2536+ // CRITICAL: Explicitly IGNORE speech_style updates
2537+ if (updates .speech_style ) {
2538+ logDebug (` [AI Character Progression] BLOCKED attempt to change speech_style for '${charName }' ` )
2539+ }
2540+
2541+ updatedProfiles [charName ] = currentProfile
2542+ }
2543+ }
2544+
2545+ if (hasUpdates ) {
2546+ characterProfiles .value = updatedProfiles
2547+ }
25602548 }
25612549
25622550 // Resolve Character ID EARLY to ensure TTS gets the right name
0 commit comments