This commit is contained in:
parent
c33a997dc5
commit
57507756ec
14 changed files with 864 additions and 97 deletions
|
@ -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
82
ia-calc.go
Normal 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
206
ia-currency.go
Normal 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
83
ia-main.go
Normal 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
74
ia-wiki.go
Normal 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
|
||||
}
|
|
@ -72,6 +72,7 @@ func main() {
|
|||
initFileEngines()
|
||||
initPipedInstances()
|
||||
initMusicEngines()
|
||||
initExchangeRates()
|
||||
}
|
||||
|
||||
InitializeLanguage("en") // Initialize language before generating OpenSearch
|
||||
|
|
1
init.go
1
init.go
|
@ -71,6 +71,7 @@ func main() {
|
|||
initFileEngines()
|
||||
initPipedInstances()
|
||||
initMusicEngines()
|
||||
initExchangeRates()
|
||||
}
|
||||
|
||||
InitializeLanguage("en") // Initialize language before generating OpenSearch
|
||||
|
|
2
main.go
2
main.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
139
static/css/style-instantanswer.css
Normal file
139
static/css/style-instantanswer.css
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
47
static/js/calculator.js
Normal 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();
|
||||
}
|
||||
});
|
|
@ -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'));
|
||||
|
|
|
@ -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
84
text.go
|
@ -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()
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue