react-native-gemma-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +457 -0
- package/package.json +52 -0
- package/skills/calculator.ts +47 -0
- package/skills/deviceLocation.ts +180 -0
- package/skills/index.ts +3 -0
- package/skills/queryWikipedia.ts +96 -0
- package/skills/readCalendar.ts +74 -0
- package/skills/webSearch.ts +75 -0
- package/src/AgentOrchestrator.ts +315 -0
- package/src/BM25Scorer.ts +118 -0
- package/src/FunctionCallParser.ts +113 -0
- package/src/GemmaAgentProvider.tsx +101 -0
- package/src/InferenceEngine.ts +301 -0
- package/src/ModelManager.ts +244 -0
- package/src/SkillRegistry.ts +60 -0
- package/src/SkillSandbox.tsx +155 -0
- package/src/index.ts +52 -0
- package/src/types.ts +197 -0
- package/src/useGemmaAgent.ts +222 -0
- package/src/useModelDownload.ts +80 -0
- package/src/useSkillRegistry.ts +58 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { PermissionsAndroid, Platform } from 'react-native';
|
|
2
|
+
import Geolocation from '@react-native-community/geolocation';
|
|
3
|
+
import type { SkillManifest } from '../src/types';
|
|
4
|
+
|
|
5
|
+
// --- Offline city database (haversine nearest-match) ---
|
|
6
|
+
|
|
7
|
+
type City = [string, string, string, number, number];
|
|
8
|
+
// [city, state/region, country, lat, lng]
|
|
9
|
+
|
|
10
|
+
const CITIES: City[] = [
|
|
11
|
+
// India
|
|
12
|
+
['Jodhpur', 'Rajasthan', 'India', 26.2389, 72.9668],
|
|
13
|
+
['Jaipur', 'Rajasthan', 'India', 26.9124, 75.7873],
|
|
14
|
+
['Udaipur', 'Rajasthan', 'India', 24.5854, 73.7125],
|
|
15
|
+
['Jaisalmer', 'Rajasthan', 'India', 26.9157, 70.9083],
|
|
16
|
+
['Ajmer', 'Rajasthan', 'India', 26.4499, 74.6399],
|
|
17
|
+
['Mumbai', 'Maharashtra', 'India', 19.0760, 72.8777],
|
|
18
|
+
['Delhi', 'Delhi', 'India', 28.6139, 77.2090],
|
|
19
|
+
['Bangalore', 'Karnataka', 'India', 12.9716, 77.5946],
|
|
20
|
+
['Hyderabad', 'Telangana', 'India', 17.3850, 78.4867],
|
|
21
|
+
['Chennai', 'Tamil Nadu', 'India', 13.0827, 80.2707],
|
|
22
|
+
['Kolkata', 'West Bengal', 'India', 22.5726, 88.3639],
|
|
23
|
+
['Pune', 'Maharashtra', 'India', 18.5204, 73.8567],
|
|
24
|
+
['Ahmedabad', 'Gujarat', 'India', 23.0225, 72.5714],
|
|
25
|
+
['Surat', 'Gujarat', 'India', 21.1702, 72.8311],
|
|
26
|
+
['Lucknow', 'Uttar Pradesh', 'India', 26.8467, 80.9462],
|
|
27
|
+
['Kanpur', 'Uttar Pradesh', 'India', 26.4499, 80.3319],
|
|
28
|
+
['Nagpur', 'Maharashtra', 'India', 21.1458, 79.0882],
|
|
29
|
+
['Indore', 'Madhya Pradesh', 'India', 22.7196, 75.8577],
|
|
30
|
+
['Bhopal', 'Madhya Pradesh', 'India', 23.2599, 77.4126],
|
|
31
|
+
['Patna', 'Bihar', 'India', 25.6093, 85.1376],
|
|
32
|
+
['Chandigarh', 'Chandigarh', 'India', 30.7333, 76.7794],
|
|
33
|
+
['Guwahati', 'Assam', 'India', 26.1445, 91.7362],
|
|
34
|
+
['Kochi', 'Kerala', 'India', 9.9312, 76.2673],
|
|
35
|
+
['Coimbatore', 'Tamil Nadu', 'India', 11.0168, 76.9558],
|
|
36
|
+
['Varanasi', 'Uttar Pradesh', 'India', 25.3176, 83.0064],
|
|
37
|
+
['Amritsar', 'Punjab', 'India', 31.6340, 74.8723],
|
|
38
|
+
['Agra', 'Uttar Pradesh', 'India', 27.1767, 78.0081],
|
|
39
|
+
['Dehradun', 'Uttarakhand', 'India', 30.3165, 78.0322],
|
|
40
|
+
['Goa', 'Goa', 'India', 15.2993, 74.1240],
|
|
41
|
+
['Thiruvananthapuram', 'Kerala', 'India', 8.5241, 76.9366],
|
|
42
|
+
// Global
|
|
43
|
+
['New York', 'NY', 'USA', 40.7128, -74.0060],
|
|
44
|
+
['San Francisco', 'CA', 'USA', 37.7749, -122.4194],
|
|
45
|
+
['Los Angeles', 'CA', 'USA', 34.0522, -118.2437],
|
|
46
|
+
['Chicago', 'IL', 'USA', 41.8781, -87.6298],
|
|
47
|
+
['London', '', 'UK', 51.5074, -0.1278],
|
|
48
|
+
['Paris', '', 'France', 48.8566, 2.3522],
|
|
49
|
+
['Berlin', '', 'Germany', 52.5200, 13.4050],
|
|
50
|
+
['Tokyo', '', 'Japan', 35.6762, 139.6503],
|
|
51
|
+
['Beijing', '', 'China', 39.9042, 116.4074],
|
|
52
|
+
['Shanghai', '', 'China', 31.2304, 121.4737],
|
|
53
|
+
['Singapore', '', 'Singapore', 1.3521, 103.8198],
|
|
54
|
+
['Dubai', '', 'UAE', 25.2048, 55.2708],
|
|
55
|
+
['Sydney', 'NSW', 'Australia', -33.8688, 151.2093],
|
|
56
|
+
['Toronto', 'ON', 'Canada', 43.6532, -79.3832],
|
|
57
|
+
['Seoul', '', 'South Korea', 37.5665, 126.9780],
|
|
58
|
+
['Bangkok', '', 'Thailand', 13.7563, 100.5018],
|
|
59
|
+
['Istanbul', '', 'Turkey', 41.0082, 28.9784],
|
|
60
|
+
['Moscow', '', 'Russia', 55.7558, 37.6173],
|
|
61
|
+
['São Paulo', '', 'Brazil', -23.5505, -46.6333],
|
|
62
|
+
['Mexico City', '', 'Mexico', 19.4326, -99.1332],
|
|
63
|
+
['Cairo', '', 'Egypt', 30.0444, 31.2357],
|
|
64
|
+
['Lagos', '', 'Nigeria', 6.5244, 3.3792],
|
|
65
|
+
['Nairobi', '', 'Kenya', -1.2921, 36.8219],
|
|
66
|
+
['Karachi', 'Sindh', 'Pakistan', 24.8607, 67.0011],
|
|
67
|
+
['Lahore', 'Punjab', 'Pakistan', 31.5497, 74.3436],
|
|
68
|
+
['Dhaka', '', 'Bangladesh', 23.8103, 90.4125],
|
|
69
|
+
['Colombo', '', 'Sri Lanka', 6.9271, 79.8612],
|
|
70
|
+
['Kathmandu', '', 'Nepal', 27.7172, 85.3240],
|
|
71
|
+
['Amsterdam', '', 'Netherlands', 52.3676, 4.9041],
|
|
72
|
+
['Barcelona', '', 'Spain', 41.3874, 2.1686],
|
|
73
|
+
['Rome', '', 'Italy', 41.9028, 12.4964],
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function haversineKm(
|
|
77
|
+
lat1: number,
|
|
78
|
+
lon1: number,
|
|
79
|
+
lat2: number,
|
|
80
|
+
lon2: number,
|
|
81
|
+
): number {
|
|
82
|
+
const R = 6371;
|
|
83
|
+
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
|
84
|
+
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
|
85
|
+
const a =
|
|
86
|
+
Math.sin(dLat / 2) ** 2 +
|
|
87
|
+
Math.cos((lat1 * Math.PI) / 180) *
|
|
88
|
+
Math.cos((lat2 * Math.PI) / 180) *
|
|
89
|
+
Math.sin(dLon / 2) ** 2;
|
|
90
|
+
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function findNearestCity(
|
|
94
|
+
lat: number,
|
|
95
|
+
lng: number,
|
|
96
|
+
): { name: string; distKm: number } | null {
|
|
97
|
+
let best: { name: string; distKm: number } | null = null;
|
|
98
|
+
|
|
99
|
+
for (const [city, region, country, cLat, cLng] of CITIES) {
|
|
100
|
+
const d = haversineKm(lat, lng, cLat, cLng);
|
|
101
|
+
if (!best || d < best.distKm) {
|
|
102
|
+
const parts = [city, region, country].filter(Boolean);
|
|
103
|
+
best = { name: parts.join(', '), distKm: d };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Only return if within 50km of a known city
|
|
108
|
+
return best && best.distKm <= 50 ? best : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- Skill ---
|
|
112
|
+
|
|
113
|
+
export const deviceLocationSkill: SkillManifest = {
|
|
114
|
+
name: 'device_location',
|
|
115
|
+
description:
|
|
116
|
+
'Get the current GPS location of the device including the city/area name. Works completely offline.',
|
|
117
|
+
version: '1.2.0',
|
|
118
|
+
type: 'native',
|
|
119
|
+
requiresNetwork: false,
|
|
120
|
+
parameters: {},
|
|
121
|
+
requiredParameters: [],
|
|
122
|
+
instructions:
|
|
123
|
+
'Use this when the user asks where they are, their current location, or anything about their physical position. No parameters needed. IMPORTANT: Always include ALL details from the result in your response — location name, coordinates, accuracy, and altitude. Do not summarize or omit fields.',
|
|
124
|
+
execute: async () => {
|
|
125
|
+
try {
|
|
126
|
+
if (Platform.OS === 'android') {
|
|
127
|
+
const granted = await PermissionsAndroid.request(
|
|
128
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
|
129
|
+
{
|
|
130
|
+
title: 'Location Permission',
|
|
131
|
+
message: 'This app needs access to your location.',
|
|
132
|
+
buttonPositive: 'OK',
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
|
|
136
|
+
return { error: 'Location permission denied by user.' };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const position = await new Promise<{
|
|
141
|
+
coords: {
|
|
142
|
+
latitude: number;
|
|
143
|
+
longitude: number;
|
|
144
|
+
altitude: number | null;
|
|
145
|
+
accuracy: number;
|
|
146
|
+
speed: number | null;
|
|
147
|
+
};
|
|
148
|
+
}>((resolve, reject) => {
|
|
149
|
+
Geolocation.getCurrentPosition(resolve, reject, {
|
|
150
|
+
enableHighAccuracy: true,
|
|
151
|
+
timeout: 15000,
|
|
152
|
+
maximumAge: 60000,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const { latitude, longitude, accuracy, altitude } = position.coords;
|
|
157
|
+
|
|
158
|
+
// Offline city lookup — no internet needed
|
|
159
|
+
const nearest = findNearestCity(latitude, longitude);
|
|
160
|
+
|
|
161
|
+
const parts: string[] = [];
|
|
162
|
+
if (nearest) {
|
|
163
|
+
parts.push(`Location: ${nearest.name}`);
|
|
164
|
+
}
|
|
165
|
+
parts.push(
|
|
166
|
+
`Coordinates: ${latitude.toFixed(6)}°N, ${longitude.toFixed(6)}°E`,
|
|
167
|
+
);
|
|
168
|
+
parts.push(`Accuracy: ${Math.round(accuracy)}m`);
|
|
169
|
+
if (altitude !== null) {
|
|
170
|
+
parts.push(`Altitude: ${Math.round(altitude)}m`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { result: parts.join('\n') };
|
|
174
|
+
} catch (err: unknown) {
|
|
175
|
+
const msg =
|
|
176
|
+
err instanceof Error ? err.message : 'Failed to get location';
|
|
177
|
+
return { error: `Location error: ${msg}` };
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
package/skills/index.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { SkillManifest } from '../src/types';
|
|
2
|
+
|
|
3
|
+
export const queryWikipediaSkill: SkillManifest = {
|
|
4
|
+
name: 'query_wikipedia',
|
|
5
|
+
description: 'Search Wikipedia for factual information about any topic.',
|
|
6
|
+
version: '1.1.0',
|
|
7
|
+
type: 'js',
|
|
8
|
+
requiresNetwork: true,
|
|
9
|
+
parameters: {
|
|
10
|
+
query: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'The search query to send to Wikipedia',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
requiredParameters: ['query'],
|
|
16
|
+
instructions:
|
|
17
|
+
'Use this when the user asks a factual question you are not confident about. Pass a clear, concise search query. Use the returned information to answer naturally.',
|
|
18
|
+
html: `<!DOCTYPE html>
|
|
19
|
+
<html>
|
|
20
|
+
<head><meta charset="utf-8"></head>
|
|
21
|
+
<body>
|
|
22
|
+
<script>
|
|
23
|
+
function stripLatex(text) {
|
|
24
|
+
if (!text) return text;
|
|
25
|
+
// Remove display math $$...$$
|
|
26
|
+
text = text.replace(/\\$\\$[^$]*\\$\\$/g, '');
|
|
27
|
+
// Remove inline math $...$ (but not dollar amounts like $5)
|
|
28
|
+
text = text.replace(/\\$[^$\\d][^$]*\\$/g, '');
|
|
29
|
+
// Replace \\frac{a}{b} with a/b
|
|
30
|
+
text = text.replace(/\\\\frac\\{([^}]*)\\}\\{([^}]*)\\}/g, '$1/$2');
|
|
31
|
+
// Replace \\text{...}, \\mathrm{...}, etc. with contents
|
|
32
|
+
text = text.replace(/\\\\(text|mathrm|mathbf|mathit|mathbb|mathcal|operatorname)\\{([^}]*)\\}/g, '$2');
|
|
33
|
+
// Replace \\sqrt{x} with sqrt(x)
|
|
34
|
+
text = text.replace(/\\\\sqrt\\{([^}]*)\\}/g, 'sqrt($1)');
|
|
35
|
+
// Remove \\displaystyle and similar
|
|
36
|
+
text = text.replace(/\\\\(displaystyle|textstyle|scriptstyle|left|right|Big|big)\\s*/g, '');
|
|
37
|
+
// Replace common symbols
|
|
38
|
+
text = text.replace(/\\\\times/g, 'x');
|
|
39
|
+
text = text.replace(/\\\\cdot/g, '*');
|
|
40
|
+
text = text.replace(/\\\\approx/g, '≈');
|
|
41
|
+
text = text.replace(/\\\\pm/g, '±');
|
|
42
|
+
text = text.replace(/\\\\leq/g, '<=');
|
|
43
|
+
text = text.replace(/\\\\geq/g, '>=');
|
|
44
|
+
text = text.replace(/\\\\neq/g, '!=');
|
|
45
|
+
text = text.replace(/\\\\infty/g, 'infinity');
|
|
46
|
+
text = text.replace(/\\\\sum/g, 'sum');
|
|
47
|
+
text = text.replace(/\\\\int/g, 'integral');
|
|
48
|
+
// Remove remaining \\command patterns
|
|
49
|
+
text = text.replace(/\\\\[a-zA-Z]+/g, '');
|
|
50
|
+
// Clean up leftover braces
|
|
51
|
+
text = text.replace(/[{}]/g, '');
|
|
52
|
+
// Clean up extra whitespace
|
|
53
|
+
text = text.replace(/\\s+/g, ' ').trim();
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
window['ai_edge_gallery_get_result'] = async function(jsonData) {
|
|
58
|
+
const params = JSON.parse(jsonData);
|
|
59
|
+
const query = params.query;
|
|
60
|
+
if (!query) {
|
|
61
|
+
return JSON.stringify({ error: 'No query provided' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const url = 'https://en.wikipedia.org/api/rest_v1/page/summary/'
|
|
66
|
+
+ encodeURIComponent(query);
|
|
67
|
+
const res = await fetch(url);
|
|
68
|
+
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
// Try search API as fallback
|
|
71
|
+
const searchUrl = 'https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch='
|
|
72
|
+
+ encodeURIComponent(query) + '&format=json&origin=*&srlimit=3';
|
|
73
|
+
const searchRes = await fetch(searchUrl);
|
|
74
|
+
const searchData = await searchRes.json();
|
|
75
|
+
const results = searchData.query?.search;
|
|
76
|
+
if (!results || results.length === 0) {
|
|
77
|
+
return JSON.stringify({ error: 'No Wikipedia results found for: ' + query });
|
|
78
|
+
}
|
|
79
|
+
const snippets = results.map(function(r) {
|
|
80
|
+
return r.title + ': ' + stripLatex(r.snippet.replace(/<[^>]*>/g, ''));
|
|
81
|
+
}).join('\\n\\n');
|
|
82
|
+
return JSON.stringify({ result: snippets });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
const title = data.title || query;
|
|
87
|
+
const extract = stripLatex(data.extract || 'No summary available.');
|
|
88
|
+
return JSON.stringify({ result: title + ': ' + extract });
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return JSON.stringify({ error: 'Wikipedia lookup failed: ' + e.message });
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
</script>
|
|
94
|
+
</body>
|
|
95
|
+
</html>`,
|
|
96
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import RNCalendarEvents from 'react-native-calendar-events';
|
|
2
|
+
import type { SkillManifest } from '../src/types';
|
|
3
|
+
|
|
4
|
+
export const readCalendarSkill: SkillManifest = {
|
|
5
|
+
name: 'read_calendar',
|
|
6
|
+
description:
|
|
7
|
+
"Read events from the device calendar for a specific day. Returns event titles, times, and locations. Works completely offline — reads directly from the device's local calendar.",
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
type: 'native',
|
|
10
|
+
requiresNetwork: false,
|
|
11
|
+
parameters: {
|
|
12
|
+
date: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description:
|
|
15
|
+
'Date to check in YYYY-MM-DD format. Defaults to today if not specified.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
instructions:
|
|
19
|
+
"Use this when the user asks about their schedule, calendar, meetings, appointments, or what's planned for a day. Omit the date parameter to get today's events.",
|
|
20
|
+
execute: async (params) => {
|
|
21
|
+
try {
|
|
22
|
+
const status = await RNCalendarEvents.requestPermissions();
|
|
23
|
+
if (status !== 'authorized') {
|
|
24
|
+
return { error: 'Calendar permission denied by user.' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const dateStr = params.date
|
|
29
|
+
? String(params.date)
|
|
30
|
+
: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
31
|
+
|
|
32
|
+
const startDate = new Date(`${dateStr}T00:00:00`).toISOString();
|
|
33
|
+
const endDate = new Date(`${dateStr}T23:59:59`).toISOString();
|
|
34
|
+
|
|
35
|
+
const events = await RNCalendarEvents.fetchAllEvents(startDate, endDate);
|
|
36
|
+
|
|
37
|
+
if (!events || events.length === 0) {
|
|
38
|
+
return { result: `No events found for ${dateStr}.` };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const formatted = events
|
|
42
|
+
.sort(
|
|
43
|
+
(a, b) =>
|
|
44
|
+
new Date(a.startDate).getTime() - new Date(b.startDate).getTime(),
|
|
45
|
+
)
|
|
46
|
+
.map((e) => {
|
|
47
|
+
const start = new Date(e.startDate);
|
|
48
|
+
const end = new Date(e.endDate);
|
|
49
|
+
const startTime = start.toLocaleTimeString([], {
|
|
50
|
+
hour: '2-digit',
|
|
51
|
+
minute: '2-digit',
|
|
52
|
+
});
|
|
53
|
+
const endTime = end.toLocaleTimeString([], {
|
|
54
|
+
hour: '2-digit',
|
|
55
|
+
minute: '2-digit',
|
|
56
|
+
});
|
|
57
|
+
let line = `${startTime}–${endTime}: ${e.title}`;
|
|
58
|
+
if (e.location) {
|
|
59
|
+
line += ` (at ${e.location})`;
|
|
60
|
+
}
|
|
61
|
+
return line;
|
|
62
|
+
})
|
|
63
|
+
.join('\n');
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
result: `Calendar events for ${dateStr} (${events.length} events):\n${formatted}`,
|
|
67
|
+
};
|
|
68
|
+
} catch (err: unknown) {
|
|
69
|
+
const msg =
|
|
70
|
+
err instanceof Error ? err.message : 'Failed to read calendar';
|
|
71
|
+
return { error: `Calendar error: ${msg}` };
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { SkillManifest } from '../src/types';
|
|
2
|
+
|
|
3
|
+
export const webSearchSkill: SkillManifest = {
|
|
4
|
+
name: 'web_search',
|
|
5
|
+
description: 'Search the web for current information using SearXNG.',
|
|
6
|
+
version: '2.0.0',
|
|
7
|
+
type: 'js',
|
|
8
|
+
requiresNetwork: true,
|
|
9
|
+
parameters: {
|
|
10
|
+
query: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'The search query',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
requiredParameters: ['query'],
|
|
16
|
+
instructions:
|
|
17
|
+
'Use this when the user asks about recent events, current information, or topics you are not sure about. Returns search results from the web.',
|
|
18
|
+
html: `<!DOCTYPE html>
|
|
19
|
+
<html>
|
|
20
|
+
<head><meta charset="utf-8"></head>
|
|
21
|
+
<body>
|
|
22
|
+
<script>
|
|
23
|
+
window['ai_edge_gallery_get_result'] = async function(jsonData) {
|
|
24
|
+
var params = JSON.parse(jsonData);
|
|
25
|
+
var query = params.query;
|
|
26
|
+
if (!query) {
|
|
27
|
+
return JSON.stringify({ error: 'No query provided' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// SearXNG public instances to try in order
|
|
31
|
+
var instances = [
|
|
32
|
+
'https://searx.be',
|
|
33
|
+
'https://search.bus-hit.me',
|
|
34
|
+
'https://searx.tiekoetter.com'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
for (var idx = 0; idx < instances.length; idx++) {
|
|
38
|
+
try {
|
|
39
|
+
var url = instances[idx] + '/search?q=' + encodeURIComponent(query)
|
|
40
|
+
+ '&format=json&language=en&safesearch=0';
|
|
41
|
+
var res = await fetch(url, {
|
|
42
|
+
headers: { 'Accept': 'application/json' }
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!res.ok) continue;
|
|
46
|
+
|
|
47
|
+
var data = await res.json();
|
|
48
|
+
|
|
49
|
+
if (!data.results || data.results.length === 0) {
|
|
50
|
+
// Try next instance if this one returned empty
|
|
51
|
+
if (idx < instances.length - 1) continue;
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
result: 'No results found for "' + query + '". Try rephrasing the query.'
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
var results = data.results.slice(0, 5).map(function(r) {
|
|
58
|
+
var snippet = r.content || '';
|
|
59
|
+
return r.title + ' (' + r.url + ')' + (snippet ? ': ' + snippet : '');
|
|
60
|
+
}).join('\\n\\n');
|
|
61
|
+
|
|
62
|
+
return JSON.stringify({ result: results });
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// Try next instance on failure
|
|
65
|
+
if (idx < instances.length - 1) continue;
|
|
66
|
+
return JSON.stringify({ error: 'Web search failed: ' + e.message });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return JSON.stringify({ error: 'All search instances unavailable.' });
|
|
71
|
+
};
|
|
72
|
+
</script>
|
|
73
|
+
</body>
|
|
74
|
+
</html>`,
|
|
75
|
+
};
|