Skip to content

Commit 70a8209

Browse files
committed
- Context Caching for supported models is now always enabled and the toggle is only visible in dev builds, since this option provides noticeable benefits and there is no practical reason to be disabled
- Added a "character progression" field in the memory to allow models to potentially "grow" characters and do character development depending on the story. - Fixed an issue that caused the model to overwrite the original character profiles. - Fixed an issue that caused the model to search for characters that were not present in the story. - Additional fixes and improvements for JSON fallback and non-fallback parsing. - Minor fix on Scarlet's built-in character profile.
1 parent ee93fc6 commit 70a8209

File tree

4 files changed

+393
-215
lines changed

4 files changed

+393
-215
lines changed

src/components/views/ChatInterface.vue

Lines changed: 85 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@
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'
377377
import prompts from '@/utils/json/prompts.json'
378378
import { marked } from 'marked'
379379
import { 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"
383383
const getHonorific = (characterName: string): string => {
@@ -403,7 +403,7 @@ const apiKey = ref(localStorage.getItem('nikke_api_key') || '')
403403
const model = ref('sonar')
404404
const mode = ref('roleplay')
405405
const tokenUsage = ref('medium')
406-
const enableContextCaching = ref(false)
406+
const enableContextCaching = ref(true)
407407
const playbackMode = ref('auto')
408408
const ttsEnabled = ref(false)
409409
const ttsEndpoint = ref('http://localhost:7851')
@@ -431,6 +431,7 @@ const openRouterModels = ref<any[]>([])
431431
const isRestoring = ref(false)
432432
const needsJsonReminder = ref(false)
433433
const 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
436437
const 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

Comments
 (0)