Init Instant Answers
Some checks failed
Run Integration Tests / test (push) Failing after 49s

This commit is contained in:
partisan 2025-06-25 23:23:33 +02:00
parent c33a997dc5
commit 57507756ec
14 changed files with 864 additions and 97 deletions

View file

@ -16,7 +16,6 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
@ -45,9 +44,6 @@ var (
"/apple-touch-icon.png",
"/apple-touch-icon-precomposed.png",
}
// Regex to extract favicon URLs from HTML
iconLinkRegex = regexp.MustCompile(`<link[^>]+rel=["'](?:icon|shortcut icon|apple-touch-icon)["'][^>]+href=["']([^"']+)["']`)
)
// Add this near the top with other vars
@ -372,7 +368,7 @@ func cacheFavicon(imageURL, imageID string) (string, bool, error) {
// }
// Debug
fmt.Printf("Downloading favicon [%s] for ID [%s]\n", imageURL, imageID)
printDebug("Downloading favicon ID: %s\n", imageID)
filename := fmt.Sprintf("%s_icon.webp", imageID)
imageCacheDir := filepath.Join(config.DriveCache.Path, "images")

82
ia-calc.go Normal file
View file

@ -0,0 +1,82 @@
package main
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// Enhanced math expression parser
func parseMathExpression(query string) (string, bool) {
// Clean and normalize the expression
query = strings.ReplaceAll(query, " ", "")
query = strings.ReplaceAll(query, ",", "")
// Regex to match valid math expressions
mathRegex := regexp.MustCompile(`^\d+(\.\d+)?([\+\-\*/\^]\d+(\.\d+)?)+$`)
if !mathRegex.MatchString(query) {
return "", false
}
// Operator precedence handling
operators := []struct {
symbol string
apply func(float64, float64) float64
}{
{"^", func(a, b float64) float64 {
result := 1.0
for i := 0; i < int(b); i++ {
result *= a
}
return result
}},
{"*", func(a, b float64) float64 { return a * b }},
{"/", func(a, b float64) float64 { return a / b }},
{"+", func(a, b float64) float64 { return a + b }},
{"-", func(a, b float64) float64 { return a - b }},
}
// Parse numbers and operators
var tokens []interface{}
current := ""
for _, char := range query {
if char >= '0' && char <= '9' || char == '.' {
current += string(char)
} else {
if current != "" {
num, _ := strconv.ParseFloat(current, 64)
tokens = append(tokens, num)
current = ""
}
tokens = append(tokens, string(char))
}
}
if current != "" {
num, _ := strconv.ParseFloat(current, 64)
tokens = append(tokens, num)
}
// Evaluate expression with operator precedence
for _, op := range operators {
for i := 1; i < len(tokens)-1; i += 2 {
if operator, ok := tokens[i].(string); ok && operator == op.symbol {
left := tokens[i-1].(float64)
right := tokens[i+1].(float64)
result := op.apply(left, right)
// Update tokens
tokens = append(tokens[:i-1], tokens[i+2:]...)
tokens = append(tokens[:i-1], append([]interface{}{result}, tokens[i-1:]...)...)
i -= 2 // Adjust index after modification
}
}
}
// Format result
result := tokens[0].(float64)
if result == float64(int(result)) {
return fmt.Sprintf("%d", int(result)), true
}
return fmt.Sprintf("%.2f", result), true
}

206
ia-currency.go Normal file
View file

@ -0,0 +1,206 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
)
// ExchangeRateCache holds currency rates with automatic refresh
var (
exchangeRates = make(map[string]float64)
lastUpdated time.Time
exchangeCacheMutex sync.RWMutex
cacheExpiration = 1 * time.Hour
allCurrencies []string
)
// CurrencyAPIResponse structure for exchange rate API
type CurrencyAPIResponse struct {
Rates map[string]float64 `json:"rates"`
}
// UpdateExchangeRates fetches and caches currency rates
func UpdateExchangeRates() error {
exchangeCacheMutex.Lock()
defer exchangeCacheMutex.Unlock()
// Use a reliable free API with good rate limits
resp, err := http.Get("https://open.er-api.com/v6/latest/USD")
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var apiResponse struct {
Result string `json:"result"`
Rates map[string]float64 `json:"rates"`
}
if err := json.Unmarshal(body, &apiResponse); err != nil {
return err
}
if apiResponse.Result != "success" {
return fmt.Errorf("API error: %s", apiResponse.Result)
}
// Cache the rates
exchangeRates = apiResponse.Rates
lastUpdated = time.Now()
// Update list of all currencies
allCurrencies = make([]string, 0, len(exchangeRates))
for currency := range exchangeRates {
allCurrencies = append(allCurrencies, currency)
}
log.Printf("Updated currency rates: %d currencies cached", len(allCurrencies))
return nil
}
// PrecacheAllCurrencyPairs pre-caches conversion rates for all currency pairs
func PrecacheAllCurrencyPairs() {
exchangeCacheMutex.RLock()
defer exchangeCacheMutex.RUnlock()
if len(exchangeRates) == 0 {
log.Println("Skipping precache - no rates available")
return
}
log.Printf("Starting precache of all currency pairs (%d currencies)", len(allCurrencies))
// We'll pre-cache the most common pairs directly
commonCurrencies := []string{"USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "CNY", "INR"}
for _, from := range commonCurrencies {
for _, to := range allCurrencies {
if from == to {
continue
}
GetExchangeRate(from, to)
}
}
log.Println("Common currency pairs precached")
}
// GetExchangeRate gets the current exchange rate with caching
func GetExchangeRate(from, to string) (float64, bool) {
// Auto-update cache if expired
if time.Since(lastUpdated) > cacheExpiration {
UpdateExchangeRates() // Ignore errors, use stale data if needed
}
exchangeCacheMutex.RLock()
defer exchangeCacheMutex.RUnlock()
// Handle same currency
if from == to {
return 1, true
}
// Convert via USD if direct rate not available
fromRate, fromExists := exchangeRates[from]
toRate, toExists := exchangeRates[to]
if !fromExists || !toExists {
return 0, false
}
// Calculate cross rate: (1 USD / fromRate) * toRate
return toRate / fromRate, true
}
// ParseCurrencyConversion detects and processes currency conversion queries
func ParseCurrencyConversion(query string) (float64, string, string, bool) {
// Match patterns like: "100 USD to EUR", "50 eur in gbp", "¥1000 to USD"
re := regexp.MustCompile(`(?i)([\d,]+(?:\.\d+)?)\s*([$€£¥₩₹₽A-Za-z]{1,6})\s+(?:to|in|➞|→)\s+([$€£¥₩₹₽A-Za-z]{1,6})`)
matches := re.FindStringSubmatch(query)
if len(matches) < 4 {
return 0, "", "", false
}
// Clean and parse amount
amountStr := strings.ReplaceAll(matches[1], ",", "")
amount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
return 0, "", "", false
}
// Normalize currency symbols
currencyMap := map[string]string{
"$": "USD", "€": "EUR", "£": "GBP", "¥": "JPY", "₩": "KRW", "₹": "INR", "₽": "RUB",
"usd": "USD", "eur": "EUR", "gbp": "GBP", "jpy": "JPY", "krw": "KRW", "inr": "INR", "rub": "RUB",
"dollar": "USD", "euro": "EUR", "pound": "GBP", "yen": "JPY", "won": "KRW", "rupee": "INR", "ruble": "RUB",
}
fromCurr := strings.ToUpper(matches[2])
if mapped, ok := currencyMap[fromCurr]; ok {
fromCurr = mapped
} else if len(fromCurr) > 3 {
// Try to match longer names
for k, v := range currencyMap {
if strings.EqualFold(k, fromCurr) {
fromCurr = v
break
}
}
}
toCurr := strings.ToUpper(matches[3])
if mapped, ok := currencyMap[toCurr]; ok {
toCurr = mapped
} else if len(toCurr) > 3 {
// Try to match longer names
for k, v := range currencyMap {
if strings.EqualFold(k, toCurr) {
toCurr = v
break
}
}
}
return amount, fromCurr, toCurr, true
}
// ConvertCurrency handles the actual conversion
func ConvertCurrency(amount float64, from, to string) (float64, bool) {
if from == to {
return amount, true
}
rate, ok := GetExchangeRate(from, to)
if !ok {
// Try to find similar currencies
from = strings.ToUpper(from)
to = strings.ToUpper(to)
// Check if we have the currency in our list
exchangeCacheMutex.RLock()
defer exchangeCacheMutex.RUnlock()
_, fromExists := exchangeRates[from]
_, toExists := exchangeRates[to]
if !fromExists || !toExists {
return 0, false
}
// Shouldn't happen due to the check above, but just in case
return 0, false
}
return amount * rate, true
}

83
ia-main.go Normal file
View file

@ -0,0 +1,83 @@
package main
import (
"fmt"
"time"
)
type InstantAnswer struct {
Type string // "calc", "unit_convert", "wiki", ...
Title string
Content interface{}
}
func detectInstantAnswer(query string) *InstantAnswer {
// Try currency conversion first (more specific)
if amount, from, to, ok := ParseCurrencyConversion(query); ok {
if result, ok := ConvertCurrency(amount, from, to); ok {
return &InstantAnswer{
Type: "currency",
Title: "Currency Conversion",
Content: map[string]interface{}{
"from": from,
"to": to,
"amount": amount,
"result": result,
"display": fmt.Sprintf("%.2f %s = %.2f %s", amount, from, result, to),
},
}
}
}
// Try math expression
if result, ok := parseMathExpression(query); ok {
return &InstantAnswer{
Type: "calc",
Title: "Calculation Result",
Content: result,
}
}
// Try Wikipedia search
if title, text, link, ok := getWikipediaSummary(query); ok {
return &InstantAnswer{
Type: "wiki",
Title: title,
Content: map[string]string{
"text": text,
"link": link,
},
}
}
return nil
}
func initExchangeRates() {
// Initial synchronous load
if err := UpdateExchangeRates(); err != nil {
printErr("Initial exchange rate update failed: %v", err)
} else {
PrecacheAllCurrencyPairs()
}
// Pre-cache common wiki terms in background
go func() {
commonTerms := []string{"United States", "Europe", "Technology", "Science", "Mathematics"}
for _, term := range commonTerms {
getWikipediaSummary(term)
}
}()
// Periodically update cache
ticker := time.NewTicker(30 * time.Minute)
go func() {
for range ticker.C {
if err := UpdateExchangeRates(); err != nil {
printWarn("Periodic exchange rate update failed: %v", err)
} else {
PrecacheAllCurrencyPairs()
}
}
}()
}

74
ia-wiki.go Normal file
View file

@ -0,0 +1,74 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
// Wikipedia API response structure
type WikipediaResponse struct {
Query struct {
Pages map[string]struct {
PageID int `json:"pageid"`
Title string `json:"title"`
Extract string `json:"extract"`
} `json:"pages"`
} `json:"query"`
}
// Get Wikipedia summary
func getWikipediaSummary(query string) (title, text, link string, ok bool) {
// Clean and prepare query
query = strings.TrimSpace(query)
if query == "" {
return "", "", "", false
}
// API request
apiURL := fmt.Sprintf(
"https://en.wikipedia.org/w/api.php?action=query&format=json&prop=extracts&exintro&explaintext&redirects=1&titles=%s",
url.QueryEscape(query),
)
resp, err := http.Get(apiURL)
if err != nil {
return "", "", "", false
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", "", false
}
// Parse JSON response
var result WikipediaResponse
if err := json.Unmarshal(body, &result); err != nil {
return "", "", "", false
}
// Extract first valid page
for _, page := range result.Query.Pages {
if page.PageID == 0 || page.Extract == "" {
continue
}
// Format text
text = page.Extract
if len(text) > 500 {
text = text[:500] + "..."
}
// Create link
titleForURL := strings.ReplaceAll(page.Title, " ", "_")
link = fmt.Sprintf("https://en.wikipedia.org/wiki/%s", url.PathEscape(titleForURL))
return page.Title, text, link, true
}
return "", "", "", false
}

View file

@ -72,6 +72,7 @@ func main() {
initFileEngines()
initPipedInstances()
initMusicEngines()
initExchangeRates()
}
InitializeLanguage("en") // Initialize language before generating OpenSearch

View file

@ -71,6 +71,7 @@ func main() {
initFileEngines()
initPipedInstances()
initMusicEngines()
initExchangeRates()
}
InitializeLanguage("en") // Initialize language before generating OpenSearch

View file

@ -175,7 +175,7 @@ func handleSearch(w http.ResponseWriter, r *http.Request) {
case "text":
fallthrough
default:
HandleTextSearch(w, settings, query, page)
HandleTextSearchWithInstantAnswer(w, settings, query, page)
}
}

View file

@ -0,0 +1,139 @@
.instant-container {
position: absolute;
top: 140px;
right: 175px;
width: 500px;
z-index: 1;
}
.instant-box {
border: 1px solid var(--snip-border);
background-color: var(--snip-background);
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.instant-box h3 {
margin-top: 0;
color: var(--highlight);
font-size: 20px;
border-bottom: 1px solid var(--border);
padding-bottom: 10px;
text-decoration: none;
}
.instant-result {
margin: 15px 0;
font-size: 16px;
line-height: 1.5;
}
.calc-container {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 15px;
}
.calc-input {
padding: 10px;
width: 95%;
font-size: 18px;
border: 1px solid var(--border);
border-radius: 4px;
background-color: var(--search-bg);
color: var(--fg);
}
.calc-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 5px;
}
.calc-buttons button {
padding: 10px;
font-size: 18px;
border: 1px solid var(--border);
border-radius: 4px;
background-color: var(--button);
color: var(--fg);
cursor: pointer;
transition: background-color 0.2s;
}
.calc-buttons button:hover {
background-color: var(--search-select);
}
.calc-result {
margin-top: 10px;
padding: 10px;
font-size: 20px;
border: 1px solid var(--border);
border-radius: 4px;
background-color: var(--snip-background);
}
.calc-history {
display: flex;
flex-direction: column-reverse;
font-family: monospace;
font-size: 16px;
color: var(--fg);
background-color: var(--search-bg);
border: 1px solid var(--border);
padding: 10px;
border-radius: 4px;
height: 120px;
overflow-y: auto;
white-space: pre-line;
overflow-anchor: none;
}
.calc-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.calc-buttons button {
padding: 20px;
font-size: 20px;
border-radius: 6px;
background-color: var(--search-bg);
border: 1px solid var(--border);
color: var(--fg);
}
.calc-buttons .equals {
background-color: #2ecc71;
color: #fff;
font-weight: bold;
}
.calc-buttons .equals:hover {
background-color: #27ae60;
}
/* Responsive adjustments */
@media only screen and (max-width: 1220px) {
.instant-container {
position: relative;
width: 100%;
top: 0;
right: 0;
margin-left: 175px;
margin-top: 20px;
}
}
@media only screen and (max-width: 880px) {
.instant-container {
width: 90%;
margin-left: 20px;
}
}

View file

@ -123,93 +123,6 @@ html {
font-size: 15px;
}
.calc {
height: fit-content;
width: fit-content;
position: relative;
left: 175px;
border: 1px solid var(--snip-border);
background-color: var(--snip-background);
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
.calc-btn-style {
background-color: var(--html-bg) !important;
}
.calc-input {
width: 90%;
height: 10%;
background-color: var(--search-bg);
border: 1px solid var(--snip-border);
border-radius: 8px;
padding: 15px;
margin-top: 8px;
text-align: right;
max-width: 48em;
line-height: 1.58;
font-size: 22px;
color: var(--fg);
letter-spacing: normal;
overflow: hidden;
}
.calc-btn {
max-width: 48em;
line-height: 50px;
font-size: 22px;
color: var(--fg);
letter-spacing: normal;
border-radius: 8px;
background-color: var(--search-bg);
border: 1px solid var(--snip-border);
height: 50px;
margin: auto;
margin: 4px;
width: 80px;
text-align: center;
}
.calc-btn-2 {
max-width: 48em;
line-height: 50px;
font-size: 22px;
color: var(--fff);
letter-spacing: normal;
border-radius: 8px;
background-color: var(--font-fg);
height: 50px;
margin: auto;
margin: 4px;
width: 80px;
text-align: center;
}
.calc-btns {
display: grid;
grid-template-columns: repeat(4, 90px);
width: 100%;
justify-content: center;
padding: 4px;
}
.calc-pos-absolute {
position: absolute;
margin-top: 60px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.prev_calculation {
opacity: 0.5;
font-size: 14px;
}
.emoji-code {
font-family: Arial, Helvetica, sans-serif;
}
@ -978,6 +891,7 @@ p {
max-width: 600px;
word-wrap: break-word;
margin-bottom: 35px;
z-index: 0;
}
.result_sublink {
@ -1518,6 +1432,10 @@ button {
@media only screen and (max-width: 1220px) {
.results {
padding-right: 340px;
}
.snip {
position: relative;
float: none;
@ -1541,10 +1459,6 @@ button {
font-size: 13px;
}
.calc {
left: 20px;
}
.results h3,
.result_sublink h3 {
font-size: 16px;

47
static/js/calculator.js Normal file
View file

@ -0,0 +1,47 @@
document.addEventListener('DOMContentLoaded', function () {
const calcContainer = document.getElementById('dynamic-calc');
const staticCalc = document.getElementById('static-calc');
if (calcContainer) {
calcContainer.style.display = 'block';
if (staticCalc) staticCalc.style.display = 'none';
const input = document.getElementById('calc-input');
const result = document.getElementById('calc-result');
const history = document.getElementById('calc-history');
const buttons = document.querySelectorAll('.calc-buttons button');
let currentInput = '';
let historyLog = [];
const updateUI = () => {
input.value = currentInput;
history.innerHTML = historyLog.map(entry => `<div>${entry}</div>`).join('');
};
buttons.forEach(button => {
button.addEventListener('click', () => {
const val = button.getAttribute('data-value');
if (val === 'C') {
currentInput = '';
} else if (val === '=') {
try {
const res = eval(currentInput);
historyLog.push(`${currentInput} = ${res}`);
if (historyLog.length > 30) historyLog.shift(); // Remove oldest
currentInput = res.toString();
} catch (e) {
currentInput = 'Error';
}
} else {
currentInput += val;
}
updateUI();
});
});
updateUI();
}
});

View file

@ -10,6 +10,7 @@
<title>{{ .Query }} - {{ translate "site_name" }}</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
<link rel="stylesheet" href="/static/css/style-instantanswer.css">
<link rel="stylesheet" href="/static/css/style-fonts.css">
<link rel="stylesheet" href="/static/css/style-loadingcircle.css">
<link rel="stylesheet" href="/static/css/style-menu.css">
@ -163,6 +164,58 @@
</form>
<p class="fetched fetched_dif_files fetched_tor">{{ translate "fetched_in" .Fetched }}</p>
<!-- Instant Answers Container -->
{{if .Instant}}
<div class="instant-container">
<div class="instant-box">
<h3>{{.Instant.Title}}</h3>
{{if eq .Instant.Type "currency"}}
<div class="instant-result">
{{index .Instant.Content "display"}}
</div>
{{end}}
{{if eq .Instant.Type "calc"}}
<div class="instant-result">
<!-- Static result for non-JS users -->
<div id="static-calc">{{.Instant.Content}}</div>
<!-- Calculator UI for JS users -->
<div id="dynamic-calc" class="calc-container" style="display:none;">
<div class="calc-history" id="calc-history"></div>
<input type="text" class="calc-input" id="calc-input" value="{{.Query}}" readonly>
<div class="calc-buttons">
<button data-value="C"></button>
<button data-value="(">(</button>
<button data-value=")">)</button>
<button data-value="%">mod</button>
<button data-value="7">7</button>
<button data-value="8">8</button>
<button data-value="9">9</button>
<button data-value="/">÷</button>
<button data-value="4">4</button>
<button data-value="5">5</button>
<button data-value="6">6</button>
<button data-value="*">×</button>
<button data-value="1">1</button>
<button data-value="2">2</button>
<button data-value="3">3</button>
<button data-value="-">-</button>
<button data-value="0">0</button>
<button data-value=".">,</button>
<button data-value="=" class="equals">=</button>
<button data-value="+">+</button>
</div>
</div>
</div>
{{end}}
{{if eq .Instant.Type "wiki"}}
<div class="instant-result">{{index .Instant.Content "text"}}</div>
<div><a href="{{index .Instant.Content "link"}}" target="_blank">Read more on Wikipedia →</a></div>
{{end}}
</div>
</div>
{{end}}
<div class="results" id="results">
{{ if .Results }}
{{ range .Results }}
@ -196,10 +249,12 @@
</div>
{{ end }}
</div>
<div id="message-bottom-right" class="message-bottom-right">
<span id="loading-text">{{ translate "searching_for_new_results" }}</span><span class="dot">.</span><span
class="dot">.</span><span class="dot">.</span>
</div>
<div class="prev-next prev-img" id="prev-next">
<form action="/search" method="get">
<input type="hidden" name="q" value="{{ .Query }}">
@ -218,6 +273,7 @@
data-hard-cache-enabled="{{ .HardCacheEnabled }}"></div>
<script defer src="/static/js/dynamicscrollingtext.js"></script>
<script defer src="/static/js/autocomplete.js"></script>
<script defer src="/static/js/calculator.js"></script>
<script defer src="/static/js/minimenu.js"></script>
<script>
document.querySelectorAll('.js-enabled').forEach(el => el.classList.remove('js-enabled'));

View file

@ -36,6 +36,90 @@ func initTextEngines() {
}
}
func HandleTextSearchWithInstantAnswer(w http.ResponseWriter, settings UserSettings, query string, page int) {
startTime := time.Now()
var instant *InstantAnswer
done := make(chan struct{})
// Spawn instant detection in background
go func() {
instant = detectInstantAnswer(query)
close(done)
}()
// Call original handler and capture its result template data
cacheKey := CacheKey{
Query: query,
Page: page,
Safe: settings.SafeSearch == "active",
Lang: settings.SearchLanguage,
Type: "text",
}
combinedResults := getTextResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.SearchLanguage, page)
hasPrevPage := page > 1
go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page+1)
if hasPrevPage {
go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page-1)
}
elapsedTime := time.Since(startTime)
type DecoratedResult struct {
TextSearchResult
PrettyLink LinkParts
FaviconID string
}
var decoratedResults []DecoratedResult
for _, r := range combinedResults {
if r.URL == "" {
continue
}
prettyLink := FormatLinkHTML(r.URL)
faviconID := faviconIDFromURL(prettyLink.RootURL)
decoratedResults = append(decoratedResults, DecoratedResult{
TextSearchResult: r,
PrettyLink: prettyLink,
FaviconID: faviconID,
})
go ensureFaviconIsCached(faviconID, prettyLink.RootURL)
}
// Wait max 300ms for instant answer detection
select {
case <-done:
case <-time.After(300 * time.Millisecond):
printInfo("InstantAnswer detection timed out")
}
data := map[string]interface{}{
"Results": decoratedResults,
"Query": query,
"Fetched": FormatElapsedTime(elapsedTime),
"Page": page,
"HasPrevPage": hasPrevPage,
"HasNextPage": len(combinedResults) >= 50,
"NoResults": len(combinedResults) == 0,
"LanguageOptions": languageOptions,
"CurrentLang": settings.SearchLanguage,
"Theme": settings.Theme,
"Safe": settings.SafeSearch,
"IsThemeDark": settings.IsThemeDark,
"Trans": Translate,
"HardCacheEnabled": config.DriveCacheEnabled,
}
if instant != nil {
data["Instant"] = instant
}
renderTemplate(w, "text.html", data)
}
func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
startTime := time.Now()

84
text.go
View file

@ -36,6 +36,90 @@ func initTextEngines() {
}
}
func HandleTextSearchWithInstantAnswer(w http.ResponseWriter, settings UserSettings, query string, page int) {
startTime := time.Now()
var instant *InstantAnswer
done := make(chan struct{})
// Spawn instant detection in background
go func() {
instant = detectInstantAnswer(query)
close(done)
}()
// Call original handler and capture its result template data
cacheKey := CacheKey{
Query: query,
Page: page,
Safe: settings.SafeSearch == "active",
Lang: settings.SearchLanguage,
Type: "text",
}
combinedResults := getTextResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.SearchLanguage, page)
hasPrevPage := page > 1
go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page+1)
if hasPrevPage {
go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page-1)
}
elapsedTime := time.Since(startTime)
type DecoratedResult struct {
TextSearchResult
PrettyLink LinkParts
FaviconID string
}
var decoratedResults []DecoratedResult
for _, r := range combinedResults {
if r.URL == "" {
continue
}
prettyLink := FormatLinkHTML(r.URL)
faviconID := faviconIDFromURL(prettyLink.RootURL)
decoratedResults = append(decoratedResults, DecoratedResult{
TextSearchResult: r,
PrettyLink: prettyLink,
FaviconID: faviconID,
})
go ensureFaviconIsCached(faviconID, prettyLink.RootURL)
}
// Wait max 300ms for instant answer detection
select {
case <-done:
case <-time.After(300 * time.Millisecond):
printInfo("InstantAnswer detection timed out")
}
data := map[string]interface{}{
"Results": decoratedResults,
"Query": query,
"Fetched": FormatElapsedTime(elapsedTime),
"Page": page,
"HasPrevPage": hasPrevPage,
"HasNextPage": len(combinedResults) >= 50,
"NoResults": len(combinedResults) == 0,
"LanguageOptions": languageOptions,
"CurrentLang": settings.SearchLanguage,
"Theme": settings.Theme,
"Safe": settings.SafeSearch,
"IsThemeDark": settings.IsThemeDark,
"Trans": Translate,
"HardCacheEnabled": config.DriveCacheEnabled,
}
if instant != nil {
data["Instant"] = instant
}
renderTemplate(w, "text.html", data)
}
func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
startTime := time.Now()