1+ import { ref } from 'vue'
2+ import l2d from '@/utils/json/l2d.json'
3+ import localCharacterProfiles from '@/utils/json/characterProfiles.json'
4+ import prompts from '@/utils/json/prompts.json'
5+ import { cleanWikiContent } from '@/utils/chatUtils'
6+
7+ // Web search state
8+ export const allowWebSearchFallback = ref ( localStorage . getItem ( 'nikke_allow_web_search_fallback' ) === 'true' )
9+
10+ // Constants
11+ export const NATIVE_SEARCH_PREFIXES = [ 'openai/' , 'anthropic/' , 'perplexity/' , 'x-ai/' ]
12+ export const POLLINATIONS_NATIVE_SEARCH_MODELS = [ 'gemini-search' , 'perplexity-fast' , 'perplexity-reasoning' ]
13+ export const WIKI_PROXY_URL = 'https://nikke-wiki-proxy.rhysticone.workers.dev'
14+
15+ // Helper functions
16+ export const hasNativeSearch = ( modelId : string ) => NATIVE_SEARCH_PREFIXES . some ( ( prefix ) => modelId . startsWith ( prefix ) )
17+
18+ export const usesWikiFetch = ( apiProvider : string , model : string ) => apiProvider === 'openrouter' && ! hasNativeSearch ( model )
19+
20+ export const usesPollinationsAutoFallback = ( apiProvider : string , model : string ) => {
21+ if ( apiProvider !== 'pollinations' ) return false
22+ // These models should NOT have auto-enabled fallback
23+ return ! POLLINATIONS_NATIVE_SEARCH_MODELS . includes ( model )
24+ }
25+
26+ export const webSearchFallbackHelpText = ( usesWikiFetchVal : boolean , usesPollinationsAutoFallbackVal : boolean ) => {
27+ if ( usesWikiFetchVal || usesPollinationsAutoFallbackVal ) {
28+ return 'If a character is not found in the local profiles, allow the model to search the web.<br><br>Web search fallback is free for models without native search thanks to Cloudflare Workers, and is therefore always enabled with your current selection.'
29+ } else {
30+ return 'If a character is not found in the local profiles, allow the model to search the web. Note that this may incur extra costs, depending on your API provider and model.<br><br>If disabled, the model will rely on its internal knowledge for unknown characters and may degrade the experience.'
31+ }
32+ }
33+
34+ // Helper for debug logging
35+ const logDebug = ( ...args : any [ ] ) => {
36+ if ( import . meta. env . DEV ) {
37+ console . log ( ...args )
38+ }
39+ }
40+
41+ export const fetchWikiContent = async ( characterName : string ) : Promise < string | null > => {
42+ const wikiName = characterName . replace ( / / g, '_' )
43+
44+ try {
45+ // Fetch the Story page via our Cloudflare Worker proxy
46+ // The worker fetches only Description, Personality, and Backstory sections
47+ const response = await fetch ( `${ WIKI_PROXY_URL } ?page=${ encodeURIComponent ( wikiName + '/Story' ) } ` )
48+ const data = await response . json ( )
49+
50+ // Check for errors
51+ if ( data . error ) {
52+ console . warn ( `[fetchWikiContent] Wiki page not found for ${ characterName } :` , data . error )
53+ return null
54+ }
55+
56+ // Extract wikitext from parse response
57+ const wikitext = data . parse ?. wikitext ?. [ '*' ]
58+ if ( ! wikitext ) {
59+ console . warn ( `[fetchWikiContent] No content found for ${ characterName } ` )
60+ return null
61+ }
62+
63+ return wikitext
64+ } catch ( e ) {
65+ console . error ( `[fetchWikiContent] Error fetching wiki for ${ characterName } :` , e )
66+ return null
67+ }
68+ }
69+
70+ export const searchForCharactersViaWikiFetch = async (
71+ characterNames : string [ ] ,
72+ characterProfiles : Record < string , any > ,
73+ apiProvider : string ,
74+ callGemini : Function ,
75+ callOpenRouter : Function ,
76+ callPollinations : Function
77+ ) : Promise < void > => {
78+ logDebug ( '[searchForCharactersViaWikiFetch] Fetching wiki pages for:' , characterNames )
79+
80+ for ( const name of characterNames ) {
81+ if ( characterProfiles [ name ] ) continue
82+
83+ const wikiContent = await fetchWikiContent ( name )
84+ if ( ! wikiContent ) {
85+ console . warn ( `[searchForCharactersViaWikiFetch] No wiki content found for ${ name } ` )
86+ continue
87+ }
88+
89+ const cleanedContent = cleanWikiContent ( wikiContent )
90+ logDebug ( `[searchForCharactersViaWikiFetch] Fetched ${ cleanedContent . length } chars for ${ name } ` )
91+
92+ const summarizePrompt = prompts . search . wikiFetch
93+ . replace ( / { n a m e } / g, name )
94+ . replace ( '{content}' , cleanedContent )
95+
96+ const messages = [
97+ { role : 'system' , content : prompts . search . system . wikiFetch } ,
98+ { role : 'user' , content : summarizePrompt }
99+ ]
100+
101+ let attempts = 0
102+ const maxAttempts = 3
103+ let success = false
104+
105+ while ( attempts < maxAttempts && ! success ) {
106+ try {
107+ let result : string
108+
109+ if ( apiProvider === 'gemini' ) {
110+ result = await callGemini ( messages , false )
111+ } else if ( apiProvider === 'openrouter' ) {
112+ result = await callOpenRouter ( messages , false )
113+ } else if ( apiProvider === 'pollinations' ) {
114+ result = await callPollinations ( messages , false )
115+ } else {
116+ // Fallback to OpenRouter for other providers
117+ result = await callOpenRouter ( messages , false )
118+ }
119+
120+ let jsonStr = result . replace ( / ` ` ` j s o n \n ? | \n ? ` ` ` / g, '' ) . trim ( )
121+ const start = jsonStr . indexOf ( '{' )
122+ const end = jsonStr . lastIndexOf ( '}' )
123+ if ( start !== - 1 && end !== - 1 ) {
124+ jsonStr = jsonStr . substring ( start , end + 1 )
125+ }
126+
127+ const profiles = JSON . parse ( jsonStr )
128+
129+ // Add character IDs
130+ for ( const charName of Object . keys ( profiles ) ) {
131+ const char = l2d . find ( ( c ) => c . name . toLowerCase ( ) === charName . toLowerCase ( ) )
132+ if ( char ) {
133+ profiles [ charName ] . id = char . id
134+ }
135+ }
136+
137+ Object . assign ( characterProfiles , profiles )
138+ logDebug ( `[searchForCharactersViaWikiFetch] Added profile for ${ name } :` , profiles )
139+ success = true
140+ } catch ( e ) {
141+ attempts ++
142+ if ( attempts >= maxAttempts ) {
143+ console . error ( `[searchForCharactersViaWikiFetch] Failed to process ${ name } after ${ maxAttempts } attempts:` , e )
144+ } else {
145+ console . warn ( `[searchForCharactersViaWikiFetch] Attempt ${ attempts } failed for ${ name } , retrying...` )
146+ }
147+ }
148+ }
149+ }
150+ }
151+
152+ export const searchForCharactersWithNativeSearch = async (
153+ characterNames : string [ ] ,
154+ characterProfiles : Record < string , any > ,
155+ apiProvider : string ,
156+ callGemini : Function ,
157+ callOpenRouter : Function ,
158+ callPollinations : Function
159+ ) : Promise < void > => {
160+ logDebug ( '[searchForCharactersWithNativeSearch] Searching for:' , characterNames )
161+
162+ for ( const name of characterNames ) {
163+ if ( characterProfiles [ name ] ) continue
164+
165+ const wikiName = name . replace ( / / g, '_' )
166+ const storyUrl = `https://nikke-goddess-of-victory-international.fandom.com/wiki/${ wikiName } /Story`
167+
168+ const searchPrompt = prompts . search . native
169+ . replace ( / { n a m e } / g, name )
170+ . replace ( '{url}' , storyUrl )
171+
172+ const messages = [
173+ { role : 'system' , content : prompts . search . system . native } ,
174+ { role : 'user' , content : searchPrompt }
175+ ]
176+
177+ let attempts = 0
178+ const maxAttempts = 3
179+
180+ let success = false
181+
182+ while ( attempts < maxAttempts && ! success ) {
183+ attempts ++
184+ try {
185+ let result : string
186+
187+ if ( apiProvider === 'gemini' ) {
188+ result = await callGemini ( messages , true )
189+ } else if ( apiProvider === 'openrouter' ) {
190+ result = await callOpenRouter ( messages , true )
191+ } else if ( apiProvider === 'pollinations' ) {
192+ result = await callPollinations ( messages , true )
193+ } else {
194+ throw new Error ( `Unsupported provider for native search: ${ apiProvider } ` )
195+ }
196+
197+ let jsonStr = result . replace ( / ` ` ` j s o n \n ? | \n ? ` ` ` / g, '' ) . trim ( )
198+ const start = jsonStr . indexOf ( '{' )
199+ const end = jsonStr . lastIndexOf ( '}' )
200+ if ( start !== - 1 && end !== - 1 ) {
201+ jsonStr = jsonStr . substring ( start , end + 1 )
202+ }
203+
204+ const profiles = JSON . parse ( jsonStr )
205+
206+ for ( const charName of Object . keys ( profiles ) ) {
207+ const char = l2d . find ( ( c ) => c . name . toLowerCase ( ) === charName . toLowerCase ( ) )
208+ if ( char ) {
209+ profiles [ charName ] . id = char . id
210+ }
211+ }
212+
213+ Object . assign ( characterProfiles , profiles )
214+ logDebug ( `[searchForCharactersWithNativeSearch] Added profile for ${ name } :` , profiles )
215+ success = true
216+ } catch ( e ) {
217+ console . error ( `[searchForCharactersWithNativeSearch] Attempt ${ attempts } failed for ${ name } :` , e )
218+ if ( attempts < maxAttempts ) {
219+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) )
220+ } else {
221+ console . error ( `[searchForCharactersWithNativeSearch] All attempts failed for ${ name } .` )
222+ }
223+ }
224+ }
225+ }
226+ }
227+
228+ export const searchForCharactersPerplexity = async (
229+ characterNames : string [ ] ,
230+ characterProfiles : Record < string , any > ,
231+ callPerplexity : Function
232+ ) : Promise < void > => {
233+ logDebug ( '[searchForCharactersPerplexity] Searching individually for:' , characterNames )
234+
235+ for ( const name of characterNames ) {
236+ // Skip if we already have this character's profile
237+ if ( characterProfiles [ name ] ) {
238+ logDebug ( `[searchForCharactersPerplexity] Skipping ${ name } - already have profile` )
239+ continue
240+ }
241+
242+ // Build the direct wiki URL for this character's Story page
243+ // Replace spaces with underscores for wiki URL format
244+ const wikiName = name . replace ( / / g, '_' )
245+ const storyUrl = `https://nikke-goddess-of-victory-international.fandom.com/wiki/${ wikiName } /Story`
246+
247+ // Search prompt pointing directly to the Story page for personality info
248+ const searchPrompt = prompts . search . perplexity
249+ . replace ( / { n a m e } / g, name )
250+ . replace ( '{url}' , storyUrl )
251+
252+ const messages = [
253+ { role : 'system' , content : prompts . search . system . perplexity } ,
254+ { role : 'user' , content : searchPrompt }
255+ ]
256+
257+ try {
258+ const result = await callPerplexity ( messages , true , storyUrl )
259+
260+ let jsonStr = result . replace ( / ` ` ` j s o n \n ? | \n ? ` ` ` / g, '' ) . trim ( )
261+ const start = jsonStr . indexOf ( '{' )
262+ const end = jsonStr . lastIndexOf ( '}' )
263+ if ( start !== - 1 && end !== - 1 ) {
264+ jsonStr = jsonStr . substring ( start , end + 1 )
265+ }
266+
267+ const profile = JSON . parse ( jsonStr )
268+
269+ // Add character ID
270+ for ( const charName of Object . keys ( profile ) ) {
271+ const char = l2d . find ( ( c ) => c . name . toLowerCase ( ) === charName . toLowerCase ( ) )
272+ if ( char ) {
273+ profile [ charName ] . id = char . id
274+ }
275+ }
276+
277+ Object . assign ( characterProfiles , profile )
278+
279+ logDebug ( `[searchForCharactersPerplexity] Added profile for ${ name } :` , profile )
280+ } catch ( e ) {
281+ console . error ( `[searchForCharactersPerplexity] Failed to search for ${ name } :` , e )
282+ // Continue with other characters
283+ }
284+ }
285+ }
286+
287+ export const searchForCharacters = async (
288+ characterNames : string [ ] ,
289+ characterProfiles : Record < string , any > ,
290+ useLocalProfiles : boolean ,
291+ allowWebSearchFallback : boolean ,
292+ apiProvider : string ,
293+ model : string ,
294+ loadingStatus : any ,
295+ setRandomLoadingMessage : Function ,
296+ searchForCharactersPerplexity : Function ,
297+ searchForCharactersWithNativeSearch : Function ,
298+ searchForCharactersViaWikiFetch : Function
299+ ) : Promise < void > => {
300+ logDebug ( '[searchForCharacters] Searching for:' , characterNames )
301+
302+ if ( useLocalProfiles ) {
303+ loadingStatus . value = 'Searching for characters in the database...'
304+ } else {
305+ loadingStatus . value = 'Searching the web for characters...'
306+ }
307+
308+ let charsToSearch = [ ...characterNames ]
309+
310+ // Check local profiles first if enabled
311+ if ( useLocalProfiles ) {
312+ const remainingChars : string [ ] = [ ]
313+
314+ for ( const name of charsToSearch ) {
315+ // Case-insensitive lookup in local profiles
316+ const localKey = Object . keys ( localCharacterProfiles ) . find (
317+ ( k ) => k . toLowerCase ( ) === name . toLowerCase ( )
318+ )
319+
320+ if ( localKey ) {
321+ const profile = ( localCharacterProfiles as any ) [ localKey ]
322+ // Use the name requested by the AI as the key, but the data from the local profile
323+ characterProfiles [ name ] = {
324+ ...profile ,
325+ // Ensure ID is present (it is in the JSON, but fallback to l2d list just in case)
326+ id : profile . id || l2d . find ( ( c ) => c . name . toLowerCase ( ) === name . toLowerCase ( ) ) ?. id
327+ }
328+ logDebug ( `[searchForCharacters] Found local profile for ${ name } ` )
329+ } else {
330+ remainingChars . push ( name )
331+ }
332+ }
333+
334+ charsToSearch = remainingChars
335+ }
336+
337+ if ( charsToSearch . length === 0 ) {
338+ logDebug ( '[searchForCharacters] All characters found locally.' )
339+ setRandomLoadingMessage ( )
340+ return
341+ }
342+
343+ // If fallback is disabled, stop here
344+ if ( useLocalProfiles && ! allowWebSearchFallback ) {
345+ logDebug ( '[searchForCharacters] Web search fallback disabled. Skipping search for:' , charsToSearch )
346+ setRandomLoadingMessage ( )
347+ return
348+ }
349+
350+ loadingStatus . value = 'Searching the web for characters...'
351+
352+ // For Perplexity, search each character individually for better results
353+ if ( apiProvider === 'perplexity' ) {
354+ await searchForCharactersPerplexity ( charsToSearch )
355+ return
356+ }
357+
358+ // For Gemini, use native search
359+ if ( apiProvider === 'gemini' ) {
360+ await searchForCharactersWithNativeSearch ( charsToSearch )
361+ return
362+ }
363+
364+ // For OpenRouter, check if model has native search
365+ if ( apiProvider === 'openrouter' ) {
366+ if ( hasNativeSearch ( model ) ) {
367+ // Use native web search for OpenAI, Anthropic, Perplexity, xAI models
368+ await searchForCharactersWithNativeSearch ( charsToSearch )
369+ } else {
370+ // For models without native search (e.g., Claude via OpenRouter, DeepSeek, etc.)
371+ // Fetch wiki pages directly and have the model summarize
372+ await searchForCharactersViaWikiFetch ( charsToSearch )
373+ }
374+ return
375+ }
376+
377+ // For Pollinations, check if model has native search
378+ if ( apiProvider === 'pollinations' ) {
379+ if ( POLLINATIONS_NATIVE_SEARCH_MODELS . includes ( model ) ) {
380+ // Use native web search for these models
381+ await searchForCharactersWithNativeSearch ( charsToSearch )
382+ } else {
383+ // For models without native search, fetch wiki pages directly
384+ await searchForCharactersViaWikiFetch ( charsToSearch )
385+ }
386+ return
387+ }
388+ }
0 commit comments