Skip to content

Commit 78a0c14

Browse files
authored
Merge pull request #2 from Rhystic1/pollinations_api
Pollinations API support + much more
2 parents 531d0a2 + a71bd55 commit 78a0c14

File tree

8 files changed

+1930
-725
lines changed

8 files changed

+1930
-725
lines changed

src/components/views/ChatInterface.vue

Lines changed: 784 additions & 681 deletions
Large diffs are not rendered by default.

src/utils/aiWebSearchUtils.ts

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
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(/{name}/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(/```json\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(/{name}/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(/```json\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(/{name}/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(/```json\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

Comments
 (0)