feat: ai recommendations

This commit is contained in:
Miguel Ribeiro
2025-08-12 00:41:03 +02:00
parent 3ab0e73831
commit 5b2a8dea23
30 changed files with 1075 additions and 16 deletions

View File

@@ -0,0 +1,143 @@
<?php
require_once '../../includes/connect_endpoint.php';
$chatgptModelsApiUrl = 'https://api.openai.com/v1/models';
$geminiModelsApiUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
// Check if ai-type and ai-api-key are set
$aiType = isset($data["type"]) ? trim($data["type"]) : '';
$aiApiKey = isset($data["api_key"]) ? trim($data["api_key"]) : '';
$aiOllamaHost = isset($data["ollama_host"]) ? trim($data["ollama_host"]) : '';
// Validate ai-type
if (!in_array($aiType, ['chatgpt', 'gemini', 'ollama'])) {
$response = [
"success" => false,
"message" => translate('error', $i18n)
];
echo json_encode($response);
exit;
}
// Validate ai-api-key and fetch models if ai-type is chatgpt or gemini
if ($aiType === 'chatgpt' || $aiType === 'gemini') {
if (empty($aiApiKey)) {
$response = [
"success" => false,
"message" => translate('invalid_api_key', $i18n)
];
echo json_encode($response);
exit;
}
}
// Prepare the request headers
$headers = [
'Content-Type: application/json',
];
if ($aiType === 'chatgpt') {
$headers[] = 'Authorization: Bearer ' . $aiApiKey;
$apiUrl = $chatgptModelsApiUrl;
} elseif ($aiType === 'gemini') {
$apiUrl = $geminiModelsApiUrl . '?key=' . urlencode($aiApiKey);
} else {
// For ollama, no API key is needed
// Check for ollama host
if (empty($aiOllamaHost)) {
$response = [
"success" => false,
"message" => translate('invalid_host', $i18n)
];
echo json_encode($response);
exit;
}
$apiUrl = $aiOllamaHost . '/api/tags';
}
// Initialize cURL
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT, 60); // Set a timeout for the request
// Execute the request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
$response = [
"success" => false,
"message" => ($aiType === 'ollama')
? translate('invalid_host', $i18n)
: translate('error', $i18n)
];
} else {
// Decode the response
$modelsData = json_decode($response, true);
if ($aiType === 'gemini' && isset($modelsData['models']) && is_array($modelsData['models'])) {
// Normalize Gemini response
$models = array_map(function ($model) {
return [
'id' => str_replace('models/', '', $model['name']),
'name' => $model['displayName'] ?? $model['name'],
];
}, $modelsData['models']);
$response = [
"success" => true,
"models" => $models
];
} elseif (isset($modelsData['data']) && is_array($modelsData['data'])) {
// OpenAI format
$models = array_map(function ($model) {
return [
'id' => $model['id'],
'name' => $model['name'] ?? $model['id'],
];
}, $modelsData['data']);
$response = [
"success" => true,
"models" => $models
];
} elseif ($aiType === 'ollama' && isset($modelsData['models']) && is_array($modelsData['models'])) {
// Normalize Ollama response
$models = array_map(function ($model) {
return [
'id' => $model['name'],
'name' => $model['name'],
];
}, $modelsData['models']);
$response = [
"success" => true,
"models" => $models
];
} else {
$response = [
"success" => false,
"message" => ($aiType === 'ollama')
? translate('invalid_host', $i18n)
: translate('invalid_api_key', $i18n)
];
}
}
// Close cURL session
curl_close($ch);
// Return the response as JSON
echo json_encode($response);
} else {
$response = [
"success" => false,
"message" => translate('invalid_request_method', $i18n)
];
echo json_encode($response);
}
} else {
$response = [
"success" => false,
"message" => translate('session_expired', $i18n)
];
echo json_encode($response);
}

View File

@@ -0,0 +1,326 @@
<?php
set_time_limit(300);
require_once '../../includes/connect_endpoint.php';
function getPricePerMonth($cycle, $frequency, $price)
{
switch ($cycle) {
case 1:
return $price * (30 / $frequency); // daily
case 2:
return $price * (4.35 / $frequency); // weekly
case 3:
return $price / $frequency; // monthly
case 4:
return $price / (12 * $frequency); // yearly
default:
return $price;
}
}
function describeFrequency($cycle, $frequency)
{
$unit = match ($cycle) {
1 => 'day',
2 => 'week',
3 => 'month',
4 => 'year',
default => 'unit'
};
if ($frequency == 1) {
return "Every $unit";
} else {
return "Every $frequency {$unit}s";
}
}
function describeCurrency($currencyId, $currencies)
{
return $currencies[$currencyId]['code'] ?? '';
}
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
// Get AI settings for the user from the database
$stmt = $db->prepare("SELECT * FROM ai_settings WHERE user_id = ?");
$stmt->bindValue(1, $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$aiSettings = $result->fetchArray(SQLITE3_ASSOC);
$stmt->close();
if (!$aiSettings) {
$response = [
"success" => false,
"message" => translate('error', $i18n)
];
echo json_encode($response);
exit;
}
$type = isset($aiSettings['type']) ? $aiSettings['type'] : '';
$enabled = isset($aiSettings['enabled']) ? (bool) $aiSettings['enabled'] : false;
$model = isset($aiSettings['model']) ? $aiSettings['model'] : '';
$host = "";
$apiKey = "";
if (!in_array($type, ['chatgpt', 'gemini', 'ollama']) || !$enabled || empty($model)) {
$response = [
"success" => false,
"message" => translate('error', $i18n)
];
echo json_encode($response);
exit;
}
if ($type == 'ollama') {
$host = isset($aiSettings['url']) ? $aiSettings['url'] : '';
if (empty($host)) {
$response = [
"success" => false,
"message" => translate('invalid_host', $i18n)
];
echo json_encode($response);
exit;
}
} else {
$apiKey = isset($aiSettings['api_key']) ? $aiSettings['api_key'] : '';
if (empty($apiKey)) {
$response = [
"success" => false,
"message" => translate('invalid_api_key', $i18n)
];
echo json_encode($response);
exit;
}
}
// We have everything we need, fetch information from the dabase to send to the AI API
// Get the categories from the database for user with ID 1
$stmt = $db->prepare("SELECT * FROM categories WHERE user_id = :user_id");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$categories = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$categories[$row['id']] = $row;
}
// Get the currencies from the database for user with ID 1
$stmt = $db->prepare("SELECT * FROM currencies WHERE user_id = :user_id");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$currencies = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$currencies[$row['id']] = $row;
}
// Get houswhold members from the database for user with ID 1
$stmt = $db->prepare("SELECT * FROM household WHERE user_id = :user_id");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$members = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$members[$row['id']] = $row;
}
// Get language from the user table
$stmt = $db->prepare("SELECT language FROM user WHERE id = :user_id");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$userLanguage = $result->fetchArray(SQLITE3_ASSOC)['language'] ?? 'en';
// Get name from includes/i18n/languages.php
require_once '../../includes/i18n/languages.php';
$userLanguageName = $languages[$userLanguage]['name'] ?? 'English';
// Get subscriptions from the database for user with ID 1
$stmt = $db->prepare("SELECT * FROM subscriptions WHERE user_id = :user_id AND inactive = 0");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$subscriptions = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$subscriptions[] = $row;
}
if (!empty($subscriptions)) {
$subscriptionsForAI = [];
foreach ($subscriptions as $row) {
if ($row['inactive'])
continue;
$price = round($row['price'], 2);
$currencyCode = $currencies[$row['currency_id']]['code'] ?? '';
$priceFormatted = $currencyCode ? "$price $currencyCode" : "$price";
$payerName = $members[$row['payer_user_id']]['name'] ?? 'Unknown';
$subscriptionsForAI[] = [
'name' => $row['name'],
'price' => $priceFormatted,
'frequency' => describeFrequency($row['cycle'], $row['frequency']),
'category' => $categories[$row['category_id']]['name'] ?? 'Uncategorized',
'payer' => $payerName
];
}
// encode
$aiDataJson = json_encode($subscriptionsForAI, JSON_PRETTY_PRINT);
} else {
$response = [
"success" => false,
"message" => translate('error', $i18n)
];
echo json_encode($response);
exit;
}
$prompt = <<<PROMPT
You are a helpful assistant designed to help users save money on digital subscriptions.
The user has shared a list of their active subscriptions across household members. For each subscription, you are given:
- Name of the service
- Price (in original currency)
- Payment frequency (e.g., every month, every year, etc.)
- Category
- Payer (which household member pays for it)
Analyze the data and give 3 to 7 smart and specific recommendations to reduce subscription costs. If possible, include estimated savings for each suggestion.
Follow these guidelines:
- Do NOT suggest switching to family or group plans unless two or more different household members are paying for the same or similar service.
- Recognize known feature overlaps, such as:
• YouTube Premium includes YouTube Music.
• Amazon Prime includes Prime Video.
• Google One, iCloud+, and Proton all offer cloud storage.
• Real Debrid, All Debrid, and Premiumize offer similar download capabilities.
- Suggest rotating or cancelling subscriptions that serve similar purposes (e.g. multiple streaming or IPTV services).
- Recommend switching from monthly to yearly plans only if it provides clear savings and the user is likely to keep the service long-term.
- Suggest looking for promo or new customer deals if a service appears overpriced.
- Only recommend cancelling rarely used services if they do not provide unique value.
Return the result as a JSON array. Each item in the array should have:
- "title": a short summary of the suggestion
- "description": a longer explanation with reasoning
- "savings": a rough estimate like "10 EUR/month" or "60 EUR/year" (if possible)
If possible, all text should be in the user's language: {$userLanguageName}. Otherwise, use English.
Do not include any other text, just the JSON output. Absolutely no additional comments or explanations.
Here is the users data:
PROMPT;
$prompt .= "\n\n" . json_encode($subscriptionsForAI, JSON_PRETTY_PRINT);
// Prepare the cURL request
$ch = curl_init();
if ($type === 'ollama') {
curl_setopt($ch, CURLOPT_URL, $host . '/api/generate');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['model' => $model, 'prompt' => $prompt, 'stream' => false]));
} else {
$headers = ['Content-Type: application/json'];
if ($type === 'chatgpt') {
$headers[] = 'Authorization: Bearer ' . $apiKey;
curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'model' => $model,
'messages' => [['role' => 'user', 'content' => $prompt]]
]));
} elseif ($type === 'gemini') {
curl_setopt(
$ch,
CURLOPT_URL,
'https://generativelanguage.googleapis.com/v1beta/models/' . urlencode($model) .
':generateContent?key=' . urlencode($apiKey)
);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'contents' => [
[
'parts' => [['text' => $prompt]]
]
]
]));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 300);
// Execute the cURL request
$reply = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
$response = [
"success" => false,
"message" => curl_error($ch)
];
echo json_encode($response);
exit;
}
// Close the cURL session
curl_close($ch);
// Try to decode the AI's JSON reply
$replyData = json_decode($reply, true); // decode into array
if ($type === 'chatgpt' && isset($replyData['choices'][0]['message']['content'])) {
$recommendationsJson = $replyData['choices'][0]['message']['content'];
$recommendations = json_decode($recommendationsJson, true);
} elseif ($type === 'gemini' && isset($replyData['candidates'][0]['content']['parts'][0]['text'])) {
$recommendationsJson = $replyData['candidates'][0]['content']['parts'][0]['text'];
$recommendations = json_decode($recommendationsJson, true);
} else {
$recommendations = json_decode($replyData['response'], true);
}
if (json_last_error() === JSON_ERROR_NONE && is_array($recommendations)) {
// Remove old recommendations for this user
$stmt = $db->prepare("DELETE FROM ai_recommendations WHERE user_id = :user_id");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->execute();
// Insert each new recommendation
$insert = $db->prepare("
INSERT INTO ai_recommendations (user_id, type, title, description, savings)
VALUES (:user_id, :type, :title, :description, :savings)
");
foreach ($recommendations as $rec) {
$insert->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$insert->bindValue(':type', 'subscription', SQLITE3_TEXT); // or any category you want
$insert->bindValue(':title', $rec['title'] ?? '', SQLITE3_TEXT);
$insert->bindValue(':description', $rec['description'] ?? '', SQLITE3_TEXT);
$insert->bindValue(':savings', $rec['savings'] ?? '', SQLITE3_TEXT);
$insert->execute();
}
$response = [
"success" => true,
"message" => translate('success', $i18n),
"recommendations" => $recommendations
];
echo json_encode($response);
exit;
} else {
$response = [
"success" => false,
"message" => translate('error', $i18n),
"json_error" => json_last_error_msg()
];
echo json_encode($response);
exit;
}
} else {
$response = [
"success" => false,
"message" => translate('session_expired', $i18n)
];
echo json_encode($response);
exit;
}

View File

@@ -0,0 +1,99 @@
<?php
require_once '../../includes/connect_endpoint.php';
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
$aiEnabled = isset($data['ai_enabled']) ? (bool) $data['ai_enabled'] : false;
$aiType = isset($data['ai_type']) ? trim($data['ai_type']) : '';
$aiApiKey = isset($data['api_key']) ? trim($data['api_key']) : '';
$aiOllamaHost = isset($data['ollama_host']) ? trim($data['ollama_host']) : '';
$aiModel = isset($data['model']) ? trim($data['model']) : '';
if (empty($aiType) || !in_array($aiType, ['chatgpt', 'gemini', 'ollama'])) {
$response = [
"success" => false,
"message" => translate('error', $i18n)
];
echo json_encode($response);
exit;
}
if (($aiType === 'chatgpt' || $aiType === 'gemini') && empty($aiApiKey)) {
$response = [
"success" => false,
"message" => translate('invalid_api_key', $i18n)
];
echo json_encode($response);
exit;
}
if ($aiType === 'ollama' && empty($aiOllamaHost)) {
$response = [
"success" => false,
"message" => translate('invalid_host', $i18n)
];
echo json_encode($response);
exit;
}
if (empty($aiModel)) {
$response = [
"success" => false,
"message" => translate('fill_mandatory_fields', $i18n)
];
echo json_encode($response);
exit;
}
if ($aiType === 'ollama') {
$aiApiKey = ''; // Ollama does not require an API key
} else {
$aiOllamaHost = ''; // Clear Ollama host if not using Ollama
}
// Remove existing AI settings for the user
$stmt = $db->prepare("DELETE FROM ai_settings WHERE user_id = ?");
$stmt->bindValue(1, $userId, SQLITE3_INTEGER);
$stmt->execute();
$stmt->close();
// Insert new AI settings
$stmt = $db->prepare("INSERT INTO ai_settings (user_id, type, enabled, api_key, model, url) VALUES (:user_id, :type, :enabled, :api_key, :model, :url)");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->bindValue(':type', $aiType, SQLITE3_TEXT);
$stmt->bindValue(':enabled', $aiEnabled, SQLITE3_INTEGER);
$stmt->bindValue(':api_key', $aiApiKey, SQLITE3_TEXT);
$stmt->bindValue(':model', $aiModel, SQLITE3_TEXT);
$stmt->bindValue(':url', $aiOllamaHost, SQLITE3_TEXT);
$result = $stmt->execute();
if ($result) {
$response = [
"success" => true,
"message" => translate('success', $i18n),
"enabled" => $aiEnabled
];
} else {
$response = [
"success" => false,
"message" => translate('error', $i18n)
];
}
echo json_encode($response);
} else {
$response = [
"success" => false,
"message" => translate('invalid_request_method', $i18n)
];
echo json_encode($response);
}
} else {
$response = [
"success" => false,
"message" => translate('session_expired', $i18n)
];
echo json_encode($response);
}

View File

@@ -230,6 +230,16 @@ $i18n = [
"get_key" => "Získejte svůj klíč na",
"get_free_fixer_api_key" => "Získat bezplatný klíč API služby Fixer",
"get_key_alternative" => "Případně můžete získat bezplatný klíč API služby Fixer od",
"ai_model" => "AI Model",
"select_ai_model" => "Vybrat AI Model",
"run_schedule" => "Spustit plán",
"manually" => "Manuálně",
"coming_soon" => "Brzy bude k dispozici",
"invalid_host" => "Neplatný hostitel",
"ai_recommendations_info" => "AI Doporučení jsou generována na základě vašich předplatných a členů domácnosti.",
"may_take_time" => "V závislosti na poskytovateli, modelu a počtu předplatných může generování doporučení trvat nějakou dobu.",
"recommendations_visible_on_dashboard" => "Doporučení budou viditelná na řídicím panelu.",
"generate_recommendations" => "Generovat doporučení",
"display_settings" => "Nastavení zobrazení",
"theme_settings" => "Nastavení motivu",
"colors" => "Barvy",

View File

@@ -230,6 +230,16 @@ $i18n = [
"get_key" => "Få din nøgle på",
"get_free_fixer_api_key" => "Få gratis Fixer API-nøgle",
"get_key_alternative" => "Alternativt kan du få en gratis fixer API-nøgle fra",
"ai_model" => "AI Model",
"select_ai_model" => "Vælg AI Model",
"run_schedule" => "Kør tidsplan",
"manually" => "Manuelt",
"coming_soon" => "Kommer snart",
"invalid_host" => "Ugyldig vært",
"ai_recommendations_info" => "AI anbefalinger genereres baseret på dine abonnementer og husstandsmedlemmer.",
"may_take_time" => "Afhængigt af udbyderen, modellen og antallet af abonnementer kan genereringen af anbefalinger tage noget tid.",
"recommendations_visible_on_dashboard" => "Anbefalinger vil være synlige på instrumentbrættet.",
"generate_recommendations" => "Generer anbefalinger",
"display_settings" => "Visningsindstillinger",
"theme_settings" => "Temaindstillinger",
"colors" => "Farver",

View File

@@ -229,7 +229,17 @@ $i18n = [
"get_key" => "Erhalte deinen key bei",
"get_free_fixer_api_key" => "Erhalte deinen kostenfreien Fixer API Key",
"get_key_alternative" => "Alternativ können Sie einen kostenlosen Fixer-Api-Schlüssel erhalten von",
"display_settings" => "Display-Einstellungen",
"ai_model" => "AI Modell",
"select_ai_model" => "Wählen Sie AI Modell",
"run_schedule" => "Zeitplan ausführen",
"manually" => "Manuell",
"coming_soon" => "Demnächst",
"invalid_host" => "Ungültiger Host",
"ai_recommendations_info" => "AI Empfehlungen werden basierend auf Ihren Abonnements und Haushaltsmitgliedern generiert.",
"may_take_time" => "Je nach Anbieter, Modell und Anzahl der Abonnements kann die Generierung von Empfehlungen einige Zeit in Anspruch nehmen.",
"recommendations_visible_on_dashboard" => "Empfehlungen werden auf dem Dashboard sichtbar sein.",
"generate_recommendations" => "Empfehlungen generieren",
"display_settings" => "Anzeigeeinstellungen",
"theme_settings" => "Themen-Einstellungen",
"colors" => "Farben",
"custom_colors" => "Benutzerdefinierte Farben",

View File

@@ -229,6 +229,16 @@ $i18n = [
"get_key" => "Απόκτησε το κλειδί στο",
"get_free_fixer_api_key" => "Απόκτησε ΔΩΡΕΑΝ Fixer API κλειδί",
"get_key_alternative" => "Εναλλακτικά, μπορείτε να λάβετε ένα δωρεάν κλειδί api fixer από το",
"ai_model" => "AI Μοντέλο",
"select_ai_model" => "Επιλέξτε AI Μοντέλο",
"run_schedule" => "Εκτέλεση προγράμματος",
"manually" => "Χειροκίνητα",
"coming_soon" => "Έρχεται σύντομα",
"invalid_host" => "Μη έγκυρος διακομιστής",
"ai_recommendations_info" => "AI προτάσεις δημιουργούνται με βάση τα συνδρομητικά σας και τα μέλη του νοικοκυριού σας.",
"may_take_time" => "Ανάλογα με τον πάροχο, το μοντέλο και τον αριθμό των συνδρομών, η δημιουργία προτάσεων μπορεί να διαρκέσει κάποιο χρόνο.",
"recommendations_visible_on_dashboard" => "Οι προτάσεις θα είναι ορατές στον πίνακα ελέγχου.",
"generate_recommendations" => "Δημιουργία προτάσεων",
"display_settings" => "Ρυθμίσεις εμφάνισης",
"theme_settings" => "Ρυθμίσεις θέματος",
"colors" => "Χρώματα",

View File

@@ -230,6 +230,16 @@ $i18n = [
"get_key" => "Get your key at",
"get_free_fixer_api_key" => "Get free Fixer API Key",
"get_key_alternative" => "Alternatively, you can get a free fixer api key from",
"ai_model" => "AI Model",
"select_ai_model" => "Select AI Model",
"run_schedule" => "Run Schedule",
"manually" => "Manually",
"coming_soon" => "Coming Soon",
"invalid_host" => "Invalid Host",
"ai_recommendations_info" => "AI Recommendations are generated based on your subscriptions and household members.",
"may_take_time" => "Depending on the provider, model and number of subscriptions, recommendations generation may take some time.",
"recommendations_visible_on_dashboard" => "Recommendations will be visible on the dashboard.",
"generate_recommendations" => "Generate Recommendations",
"display_settings" => "Display Settings",
"theme_settings" => "Theme Settings",
"colors" => "Colors",

View File

@@ -229,6 +229,16 @@ $i18n = [
"get_key" => "Obtén tu clave en",
"get_free_fixer_api_key" => "Obtén una API Key de Fixer gratuita",
"get_key_alternative" => "También puede obtener una clave api gratuita de Fixer en",
"ai_model" => "Modelo de IA",
"select_ai_model" => "Seleccionar Modelo de IA",
"run_schedule" => "Ejecutar Programa",
"manually" => "Manualmente",
"coming_soon" => "Próximamente",
"invalid_host" => "Host Inválido",
"ai_recommendations_info" => "Las recomendaciones de IA se generan en función de sus suscripciones y miembros del hogar.",
"may_take_time" => "Dependiendo del proveedor, modelo y número de suscripciones, la generación de recomendaciones puede tardar algún tiempo.",
"recommendations_visible_on_dashboard" => "Las recomendaciones serán visibles en el panel.",
"generate_recommendations" => "Generar Recomendaciones",
"display_settings" => "Configuración de Pantalla",
"theme_settings" => "Configuración de Tema",
"colors" => "Colores",

View File

@@ -229,6 +229,16 @@ $i18n = [
"get_key" => "Obtenez votre clé sur",
"get_free_fixer_api_key" => "Obtenez une clé API Fixer gratuite",
"get_key_alternative" => "Vous pouvez également obtenir une clé api de fixation gratuite auprès de",
"ai_model" => "Modèle AI",
"select_ai_model" => "Sélectionner le modèle AI",
"run_schedule" => "Exécuter le programme",
"manually" => "Manuellement",
"coming_soon" => "À venir",
"invalid_host" => "Hôte invalide",
"ai_recommendations_info" => "Les recommandations de l'IA sont générées en fonction de vos abonnements et des membres de votre foyer.",
"may_take_time" => "En fonction du fournisseur, du modèle et du nombre d'abonnements, la génération de recommandations peut prendre un certain temps.",
"recommendations_visible_on_dashboard" => "Les recommandations seront visibles sur le tableau de bord.",
"generate_recommendations" => "Générer des recommandations",
"display_settings" => "Paramètres d'affichage",
"theme_settings" => "Paramètres de thème",
"colors" => "Couleurs",

View File

@@ -230,6 +230,16 @@ $i18n = [
"get_key" => "Dapatkan kunci Anda di",
"get_free_fixer_api_key" => "Dapatkan Kunci API Fixer gratis",
"get_key_alternative" => "Sebagai alternatif, Anda bisa mendapatkan kunci api fixer gratis dari",
"ai_model" => "Model AI",
"select_ai_model" => "Pilih Model AI",
"run_schedule" => "Jadwalkan Eksekusi",
"manually" => "Secara Manual",
"coming_soon" => "Segera Hadir",
"invalid_host" => "Host Tidak Valid",
"ai_recommendations_info" => "Rekomendasi AI dihasilkan berdasarkan langganan dan anggota rumah tangga Anda.",
"may_take_time" => "Bergantung pada penyedia, model, dan jumlah langganan, pembuatan rekomendasi mungkin memerlukan waktu.",
"recommendations_visible_on_dashboard" => "Rekomendasi akan terlihat di dasbor.",
"generate_recommendations" => "Hasilkan Rekomendasi",
"display_settings" => "Pengaturan Tampilan",
"theme_settings" => "Pengaturan Tema",
"colors" => "Warna",

View File

@@ -237,6 +237,16 @@ $i18n = [
"get_key" => 'Ottieni la tua chiave su',
"get_free_fixer_api_key" => 'Ottieni gratuitamente la chiave API di Fixer',
"get_key_alternative" => 'In alternativa, puoi ottenere gratuitamente una chiave API di Fixer da',
"ai_model" => "Modello AI",
"select_ai_model" => "Seleziona Modello AI",
"run_schedule" => "Esegui Programma",
"manually" => "Manuale",
"coming_soon" => "In Arrivo",
"invalid_host" => "Host Non Valido",
"ai_recommendations_info" => "Le raccomandazioni dell'IA sono generate in base ai tuoi abbonamenti e ai membri del tuo nucleo familiare.",
"may_take_time" => "A seconda del fornitore, del modello e del numero di abbonamenti, la generazione delle raccomandazioni potrebbe richiedere del tempo.",
"recommendations_visible_on_dashboard" => "Le raccomandazioni saranno visibili sul dashboard.",
"generate_recommendations" => "Genera Raccomandazioni",
"display_settings" => 'Impostazioni di visualizzazione',
"theme_settings" => 'Impostazioni del tema',
"colors" => 'Colori',

View File

@@ -230,6 +230,16 @@ $i18n = [
"get_key" => "キーを入手する",
"get_free_fixer_api_key" => "無料のFixer APIキーを取得",
"get_key_alternative" => "または、以下のサイトから無料のフィクサーapiキーを入手することもできます。",
"ai_model" => "AIモデル",
"select_ai_model" => "AIモデルを選択",
"run_schedule" => "スケジュールを実行",
"manually" => "手動",
"coming_soon" => "近日公開",
"invalid_host" => "無効なホスト",
"ai_recommendations_info" => "AIによる推奨は、あなたの定期購入と世帯メンバーに基づいて生成されます。",
"may_take_time" => "プロバイダ、モデル、定期購入数によっては、推奨の生成に時間がかかる場合があります。",
"recommendations_visible_on_dashboard" => "推奨はダッシュボードに表示されます。",
"generate_recommendations" => "推奨",
"display_settings" => "表示設定",
"theme_settings" => "テーマ設定",
"colors" => "",

View File

@@ -229,6 +229,16 @@ $i18n = [
"get_key" => "키 얻기",
"get_free_fixer_api_key" => "무료 Fixer API 키 얻기",
"get_key_alternative" => "또는 다음 사이트에서 무료 Fixer api 키를 얻을 수 있습니다.",
"ai_model" => "AI 모델",
"select_ai_model" => "AI 모델 선택",
"run_schedule" => "일정 실행",
"manually" => "수동으로",
"coming_soon" => "곧 출시 예정",
"invalid_host" => "유효하지 않은 호스트",
"ai_recommendations_info" => "AI 추천은 사용자의 구독과 가구 구성원을 기반으로 생성됩니다.",
"may_take_time" => "제공자, 모델, 구독 수에 따라 추천 생성에 시간이 걸릴 수 있습니다.",
"recommendations_visible_on_dashboard" => "추천은 대시보드에서 확인할 수 있습니다.",
"generate_recommendations" => "추천 생성",
"display_settings" => "디스플레이 설정",
"theme_settings" => "테마 설정",
"colors" => "색상",

View File

@@ -230,6 +230,16 @@ $i18n = [
"get_key" => "Haal je sleutel op bij",
"get_free_fixer_api_key" => "Krijg gratis Fixer API-sleutel",
"get_key_alternative" => "Als alternatief kun je een gratis Fixer API-sleutel krijgen van",
"ai_model" => "AI-model",
"select_ai_model" => "Selecteer AI-model",
"run_schedule" => "Uitvoerschema",
"manually" => "Handmatig",
"coming_soon" => "Binnenkort beschikbaar",
"invalid_host" => "Ongeldige host",
"ai_recommendations_info" => "AI-aanbevelingen worden gegenereerd op basis van je abonnementen en huishoudleden.",
"may_take_time" => "Afhankelijk van de aanbieder, het model en het aantal abonnementen kan het genereren van aanbevelingen enige tijd duren.",
"recommendations_visible_on_dashboard" => "Aanbevelingen zijn zichtbaar op het dashboard.",
"generate_recommendations" => "Genereer aanbevelingen",
"display_settings" => "Weergave-instellingen",
"theme_settings" => "Thema-instellingen",
"colors" => "Kleuren",

View File

@@ -229,6 +229,16 @@ $i18n = [
"get_key" => "Zdobądź klucz na stronie",
"get_free_fixer_api_key" => "Uzyskaj bezpłatny klucz API Fixer'a",
"get_key_alternative" => "Alternatywnie, możesz uzyskać darmowy klucz api fixer'a od",
"ai_model" => "Model AI",
"select_ai_model" => "Wybierz model AI",
"run_schedule" => "Harmonogram uruchamiania",
"manually" => "Ręcznie",
"coming_soon" => "Wkrótce dostępne",
"invalid_host" => "Nieprawidłowy host",
"ai_recommendations_info" => "Rekomendacje AI są generowane na podstawie Twoich subskrypcji i członków gospodarstwa domowego.",
"may_take_time" => "W zależności od dostawcy, modelu i liczby subskrypcji, generowanie rekomendacji może zająć trochę czasu.",
"recommendations_visible_on_dashboard" => "Rekomendacje będą widoczne na pulpicie.",
"generate_recommendations" => "Generuj rekomendacje",
"display_settings" => "Ustawienia wyświetlania",
"theme_settings" => "Ustawienia motywu",
"colors" => "Kolory",

View File

@@ -229,7 +229,17 @@ $i18n = [
"get_key" => "Obtenha a sua API Key em",
"get_free_fixer_api_key" => "Obtenha a sua API Key grátis do Fixer",
"get_key_alternative" => "Como alternativa obtenha a sua API Key em",
"display_settings" => "Definições de visualização",
"ai_model" => "Modelo de IA",
"select_ai_model" => "Selecionar modelo de IA",
"run_schedule" => "Agendamento de execução",
"manually" => "Manual",
"coming_soon" => "Em breve",
"invalid_host" => "Host inválido",
"ai_recommendations_info" => "As recomendações de IA são geradas com base nas suas assinaturas e membros da família.",
"may_take_time" => "Dependendo do provedor, modelo e número de assinaturas, a geração de recomendações pode levar algum tempo.",
"recommendations_visible_on_dashboard" => "As recomendações serão visíveis no painel.",
"generate_recommendations" => "Gerar recomendações",
"display_settings" => "Definições de exibição",
"theme_settings" => "Definições de Tema",
"colors" => "Cores",
"custom_colors" => "Cores Personalizadas",

View File

@@ -229,7 +229,17 @@ $i18n = [
"get_key" => "Obtenha a sua chave em",
"get_free_fixer_api_key" => "Obtenha a sua chave API do Fixer gratuitamente",
"get_key_alternative" => "Como alternativa, você pode obter uma chave de API grátis em",
"display_settings" => "Configurações de visualização",
"ai_model" => "Modelo de IA",
"select_ai_model" => "Selecionar modelo de IA",
"run_schedule" => "Agendamento de execução",
"manually" => "Manual",
"coming_soon" => "Em breve",
"invalid_host" => "Host inválido",
"ai_recommendations_info" => "As recomendações de IA são geradas com base em suas assinaturas e membros da família.",
"may_take_time" => "Dependendo do provedor, modelo e número de assinaturas, a geração de recomendações pode levar algum tempo.",
"recommendations_visible_on_dashboard" => "As recomendações serão visíveis no painel.",
"generate_recommendations" => "Gerar recomendações",
"display_settings" => "Configurações de exibição",
"theme_settings" => "Configurações de tema",
"colors" => "Cores",
"custom_colors" => "Cores personalizadas",

View File

@@ -229,6 +229,16 @@ $i18n = [
"get_key" => "Получите ключ по адресу",
"get_free_fixer_api_key" => "Получите бесплатный ключ API Fixer",
"get_key_alternative" => "Кроме того, вы можете получить бесплатный ключ API Fixer на сайте",
"ai_model" => "Модель ИИ",
"select_ai_model" => "Выбрать модель ИИ",
"run_schedule" => "Запустить расписание",
"manually" => "Вручную",
"coming_soon" => "Скоро",
"invalid_host" => "Неверный хост",
"ai_recommendations_info" => "Рекомендации ИИ генерируются на основе ваших подписок и членов семьи.",
"may_take_time" => "В зависимости от провайдера, модели и количества подписок генерация рекомендаций может занять некоторое время.",
"recommendations_visible_on_dashboard" => "Рекомендации будут видны на панели управления.",
"generate_recommendations" => "Сгенерировать рекомендации",
"display_settings" => "Настройки отображения",
"theme_settings" => "Настройки темы",
"colors" => "Цвета",

View File

@@ -229,6 +229,16 @@ $i18n = [
"get_key" => "Pridobite svoj ključ pri",
"get_free_fixer_api_key" => "Pridobite brezplačen ključ API Fixer",
"get_key_alternative" => "Lahko pa tudi dobite brezplačni Fixer API od",
"ai_model" => "Model AI",
"select_ai_model" => "Izberite model AI",
"run_schedule" => "Zaženi urnik",
"manually" => "Ročno",
"coming_soon" => "Kmalu",
"invalid_host" => "Neveljavna gostiteljska naprava",
"ai_recommendations_info" => "Priporočila AI so generirana na podlagi vaših naročnin in članov gospodinjstva.",
"may_take_time" => "Odvisno od ponudnika, modela in števila naročnin lahko generiranje priporočil traja nekaj časa.",
"recommendations_visible_on_dashboard" => "Priporočila bodo vidna na nadzorni plošči.",
"generate_recommendations" => "Generiraj priporočila",
"display_settings" => "Nastavitve zaslona",
"theme_settings" => "Nastavitve teme",
"colors" => "Barve",

View File

@@ -229,6 +229,16 @@ $i18n = [
"get_key" => "Добијте свој кључ на",
"get_free_fixer_api_key" => "Добијте бесплатни Fixer API кључ",
"get_key_alternative" => "Алтернативно, можете добити бесплатни Fixer API кључ са",
"ai_model" => "AI Модел",
"select_ai_model" => "Изаберите AI Модел",
"run_schedule" => "Покрените распоред",
"manually" => "Ручно",
"coming_soon" => "Ускоро",
"invalid_host" => "Неважећи хост",
"ai_recommendations_info" => "AI препоруке се генеришу на основу ваших претплата и чланова домаћинства.",
"may_take_time" => "У зависности од провајдера, модела и броја претплата, генерисање препорука може потрајати.",
"recommendations_visible_on_dashboard" => "Препоруке ће бити видљиве на контролној табли.",
"generate_recommendations" => "Генериши препоруке",
"display_settings" => "Подешавања приказа",
"theme_settings" => "Подешавања теме",
"colors" => "Боје",

View File

@@ -229,6 +229,16 @@ $i18n = [
"get_key" => "Pronađite svoj ključ na",
"get_free_fixer_api_key" => "Pronađite besplatni Fixer API ključ",
"get_key_alternative" => "Alternativno, možete dobiti besplatni Fixer API ključ na",
"ai_model" => "AI Model",
"select_ai_model" => "Izaberite AI Model",
"run_schedule" => "Pokreni raspored",
"manually" => "Ručno",
"coming_soon" => "Uskoro",
"invalid_host" => "Nevažeći host",
"ai_recommendations_info" => "AI preporuke se generišu na osnovu vaših pretplata i članova domaćinstva.",
"may_take_time" => "U zavisnosti od provajdera, modela i broja pretplata, generisanje preporuka može potrajati.",
"recommendations_visible_on_dashboard" => "Preporuke će biti vidljive na kontrolnoj tabli.",
"generate_recommendations" => "Generiši preporuke",
"display_settings" => "Podešavanja prikaza",
"theme_settings" => "Podešavanja teme",
"colors" => "Boje",

View File

@@ -229,6 +229,16 @@ $i18n = [
"get_key" => "Anahtarınızı şuradan alın",
"get_free_fixer_api_key" => "Ücretsiz Fixer API Anahtarı alın",
"get_key_alternative" => "Alternatif olarak, şu adresten ücretsiz bir fixer api anahtarı edinebilirsiniz",
"ai_model" => "AI Modeli",
"select_ai_model" => "AI Modelini Seçin",
"run_schedule" => "Programı Çalıştır",
"manually" => "Manuel Olarak",
"coming_soon" => "Çok Yakında",
"invalid_host" => "Geçersiz Host",
"ai_recommendations_info" => "AI önerileri, abonelikleriniz ve hane üyeleriniz temel alınarak oluşturulur.",
"may_take_time" => "Sağlayıcıya, modele ve abonelik sayısına bağlı olarak önerilerin oluşturulması biraz zaman alabilir.",
"recommendations_visible_on_dashboard" => "Öneriler panelde görüntülenecektir.",
"generate_recommendations" => "Önerileri Oluştur",
"display_settings" => "Görüntüleme Ayarları",
"theme_settings" => "Tema Ayarları",
"colors" => "Renkler",

View File

@@ -229,6 +229,16 @@ $i18n = [
"get_key" => "Отримайте ключ за адресою",
"get_free_fixer_api_key" => "Отримайте безкоштовний ключ API Fixer",
"get_key_alternative" => "Крім того, ви можете отримати безкоштовний ключ API Fixer на сайті",
"ai_model" => "AI Модель",
"select_ai_model" => "Виберіть AI Модель",
"run_schedule" => "Запустити Розклад",
"manually" => "Вручну",
"coming_soon" => "Незабаром",
"invalid_host" => "Неправильний Хост",
"ai_recommendations_info" => "AI рекомендації створюються на основі ваших підписок та членів домогосподарства.",
"may_take_time" => "В залежності від постачальника, моделі та кількості підписок, створення рекомендацій може зайняти деякий час.",
"recommendations_visible_on_dashboard" => "Рекомендації будуть видимі на панелі приладів.",
"generate_recommendations" => "Створити рекомендації",
"display_settings" => "Налаштування відображення",
"theme_settings" => "Налаштування теми",
"colors" => "Кольори",

View File

@@ -230,6 +230,16 @@ $i18n = [
"get_key" => "Nhận khóa của bạn tại",
"get_free_fixer_api_key" => "Nhận API Key Fixer miễn phí",
"get_key_alternative" => "Ngoài ra, bạn có thể nhận API Key Fixer miễn phí từ",
"ai_model" => "Mô hình AI",
"select_ai_model" => "Chọn Mô hình AI",
"run_schedule" => "Chạy Lịch Trình",
"manually" => "Thủ Công",
"coming_soon" => "Sắp Có",
"invalid_host" => "Máy Chủ Không Hợp Lệ",
"ai_recommendations_info" => "Các đề xuất AI được tạo dựa trên các đăng ký và thành viên hộ gia đình của bạn.",
"may_take_time" => "Tùy thuộc vào nhà cung cấp, mô hình và số lượng đăng ký, việc tạo đề xuất có thể mất một khoảng thời gian.",
"recommendations_visible_on_dashboard" => "Các đề xuất sẽ hiển thị trên bảng điều khiển.",
"generate_recommendations" => "Tạo Đề Xuất",
"display_settings" => "Cài đặt hiển thị",
"theme_settings" => "Cài đặt giao diện",
"colors" => "Màu sắc",

View File

@@ -238,6 +238,16 @@ $i18n = [
"get_key" => "申请密钥",
"get_free_fixer_api_key" => "申请免费 Fixer API 密钥",
"get_key_alternative" => "或者,您也可以从以下网站获取免费的修复程序 api 密钥",
"ai_model" => "AI 模型",
"select_ai_model" => "选择 AI 模型",
"run_schedule" => "运行计划",
"manually" => "手动",
"coming_soon" => "即将推出",
"invalid_host" => "无效的主机",
"ai_recommendations_info" => "AI 推荐是基于您的订阅和家庭成员生成的。",
"may_take_time" => "根据提供商、模型和订阅数量,推荐生成可能需要一些时间。",
"recommendations_visible_on_dashboard" => "推荐将在仪表板上可见。",
"generate_recommendations" => "生成推荐",
"display_settings" => "显示设置",
"theme_settings" => "主题设置",
"colors" => "颜色",

View File

@@ -230,7 +230,17 @@ $i18n = [
"get_key" => "取得金鑰請至",
"get_free_fixer_api_key" => "取得免費 Fixer API 金鑰",
"get_key_alternative" => "或者,您可以從以下網址取得免費的 Fixer API 金鑰",
"display_settings" => "顯示設定",
"ai_model" => "AI 模型",
"select_ai_model" => "選擇 AI 模型",
"run_schedule" => "運行計劃",
"manually" => "手動",
"coming_soon" => "即將推出",
"invalid_host" => "無效的主機",
"ai_recommendations_info" => "AI 推荐是基于您的订阅和家庭成员生成的。",
"may_take_time" => "根据提供商、模型和订阅数量,推荐生成可能需要一些时间。",
"recommendations_visible_on_dashboard" => "推荐将在仪表板上可见。",
"generate_recommendations" => "生成推荐",
"display_settings" => "显示设置",
"theme_settings" => "主題設定",
"colors" => "顏色",
"custom_colors" => "自訂顏色",

View File

@@ -887,3 +887,115 @@ var sortable = Sortable.create(el, {
saveCategorySorting();
},
});
function fetch_ai_models() {
const endpoint = 'endpoints/ai/fetch_models.php';
const type = document.querySelector("#ai_type").value;
const api_key = document.querySelector("#ai_api_key").value.trim();
const ollama_host = document.querySelector("#ai_ollama_host").value.trim();
const modelSelect = document.querySelector("#ai_model");
fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ type, api_key, ollama_host })
})
.then(response => response.json())
.then(data => {
if (data.success) {
modelSelect.innerHTML = '';
data.models.forEach(model => {
const option = document.createElement('option');
option.value = model.id;
option.textContent = model.name;
modelSelect.appendChild(option);
});
} else {
showErrorMessage(data.errorMessage);
}
})
.catch(error => {
showErrorMessage(translate('unknown_error'));
});
}
function toggleAiInputs() {
const aiTypeSelect = document.getElementById("ai_type");
const apiKeyInput = document.getElementById("ai_api_key");
const ollamaHostInput = document.getElementById("ai_ollama_host");
const type = aiTypeSelect.value;
if (type === "ollama") {
apiKeyInput.classList.add("hidden");
ollamaHostInput.classList.remove("hidden");
} else {
apiKeyInput.classList.remove("hidden");
ollamaHostInput.classList.add("hidden");
}
}
function saveAiSettingsButton() {
const aiEnabled = document.querySelector("#ai_enabled").checked;
const aiType = document.querySelector("#ai_type").value;
const aiApiKey = document.querySelector("#ai_api_key").value.trim();
const aiOllamaHost = document.querySelector("#ai_ollama_host").value.trim();
const aiModel = document.querySelector("#ai_model").value;
fetch('endpoints/ai/save_settings.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ai_enabled: aiEnabled, ai_type: aiType, api_key: aiApiKey, ollama_host: aiOllamaHost, model: aiModel })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
const runAiActionButton = document.querySelector("#runAiRecommendations");
if (data.enabled) {
runAiActionButton.classList.remove("hidden");
} else {
runAiActionButton.classList.add("hidden");
}
} else {
showErrorMessage(data.errorMessage);
}
})
.catch(error => {
showErrorMessage(translate('unknown_error'));
});
}
function runAiRecommendations() {
const endpoint = 'endpoints/ai/generate_recommendations.php';
const button = document.querySelector("#runAiRecommendations");
const spinner = document.querySelector("#aiSpinner");
button.classList.add("hidden");
spinner.classList.remove("hidden");
fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
} else {
showErrorMessage(data.errorMessage);
}
})
.catch(error => {
showErrorMessage(translate('unknown_error'));
})
.finally(() => {
button.classList.remove("hidden");
spinner.classList.add("hidden");
});
}

View File

@@ -901,6 +901,91 @@ $userData['currency_symbol'] = $currencies[$main_currency]['symbol'];
</div>
</section>
<?php
$sql = "SELECT * FROM ai_settings WHERE user_id = :userId LIMIT 1";
$stmt = $db->prepare($sql);
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$aiSettings = [];
if ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$aiSettings = $row;
}
?>
<section class="account-section">
<header>
<h2><?= translate('ai_recommendations', $i18n) ?></h2>
</header>
<div class="account-ai-settings">
<div class="form-group-inline">
<input type="checkbox" id="ai_enabled" name="ai_enabled" <?= isset($aiSettings['enabled']) && $aiSettings['enabled'] ? "checked" : "" ?>>
<label for="ai_enabled" class="capitalize"><?= translate('enabled', $i18n) ?></label>
</div>
<div class="form-group">
<label for="ai_type"><?= translate('provider', $i18n) ?>:</label>
<select id="ai_type" name="ai_type" onchange="toggleAiInputs()">
<option value="chatgpt" <?= (isset($aiSettings['type']) && $aiSettings['type'] == 'chatgpt') ? 'selected' : '' ?>>ChatGPT</option>
<option value="gemini" <?= (isset($aiSettings['type']) && $aiSettings['type'] == 'gemini') ? 'selected' : '' ?>>Gemini</option>
<option value="ollama" <?= (isset($aiSettings['type']) && $aiSettings['type'] == 'ollama') ? 'selected' : '' ?>>Local Ollama</option>
</select>
</div>
<div class="form-group-inline">
<input type="text" id="ai_api_key" name="ai_api_key"
class="<?= (isset($aiSettings['type']) && $aiSettings['type'] == 'ollama') ? 'hidden' : '' ?>"
placeholder="<?= translate('api_key', $i18n) ?>"
value="<?= isset($aiSettings['api_key']) ? htmlspecialchars($aiSettings['api_key']) : '' ?>" />
<input type="text" id="ai_ollama_host" name="ai_ollama_host"
class="<?= (!isset($aiSettings['type']) || $aiSettings['type'] != 'ollama') ? 'hidden' : '' ?>"
placeholder="<?= translate('host', $i18n) ?>"
value="<?= isset($aiSettings['url']) ? htmlspecialchars($aiSettings['url']) : '' ?>" />
<button type="button" id="fetchModelsButton" class="button thin" onclick="fetch_ai_models()">
<?= translate('test', $i18n) ?>
</button>
</div>
<div class="form-group">
<label for="ai_model"><?= translate('ai_model', $i18n) ?>:</label>
<select id="ai_model" name="ai_model">
<option value=""><?= translate('select_ai_model', $i18n) ?></option>
<?php if (!empty($aiSettings['model'])): ?>
<option value="<?= htmlspecialchars($aiSettings['model']) ?>" selected>
<?= htmlspecialchars($aiSettings['model']) ?></option>
<?php endif; ?>
</select>
</div>
<div class="form-group">
<label for="ai_run_schedule" class="flex"><?= translate('run_schedule', $i18n) ?>: <span
class="info-badge"><?= translate("coming_soon", $i18n) ?></span></span></label>
<select id="ai_run_schedule" name="ai_run_schedule" disabled>
<option value="manual" <?= (isset($aiSettings['run_schedule']) && $aiSettings['run_schedule'] == 'manual') ? 'selected' : '' ?>><?= translate('manually', $i18n) ?>
</option>
<option value="weekly" <?= (isset($aiSettings['run_schedule']) && $aiSettings['run_schedule'] == 'weekly') ? 'selected' : '' ?>><?= translate('Weekly', $i18n) ?>
</option>
<option value="monthly" <?= (isset($aiSettings['run_schedule']) && $aiSettings['run_schedule'] == 'monthly') ? 'selected' : '' ?>><?= translate('Monthly', $i18n) ?>
</option>
</select>
</div>
<div class="buttons wrap mobile-reverse">
<?php
$canBeExecuted = !empty($aiSettings['model']) && !empty($aiSettings['enabled']) && $aiSettings['enabled'] == 1;
?>
<input type="button" id="runAiRecommendations"
class="secondary-button thin mobile-grow-force <?= !$canBeExecuted ? 'hidden' : '' ?>"
onclick="runAiRecommendations()" value="<?= translate('generate_recommendations', $i18n) ?>" />
<div id="aiSpinner" class="spinner ai-spinner hidden"></div>
<input type="submit" class="thin mobile-grow-force" value="<?= translate('save', $i18n) ?>"
id="saveAiSettings" onClick="saveAiSettingsButton()" />
</div>
<div class="settings-notes">
<p><i class="fa-solid fa-circle-info"></i><?= translate('ai_recommendations_info', $i18n) ?></p>
<p><i class="fa-solid fa-circle-info"></i><?= translate('may_take_time', $i18n) ?></p>
<p><i class="fa-solid fa-circle-info"></i><?= translate('recommendations_visible_on_dashboard', $i18n) ?></p>
</div>
</div>
</section>
<?php
$sql = "SELECT * FROM payment_methods WHERE user_id = :userId ORDER BY `order` ASC";
$stmt = $db->prepare($sql);
@@ -1163,8 +1248,7 @@ $userData['currency_symbol'] = $currencies[$main_currency]['symbol'];
<div>
<div class="form-group-inline">
<input type="checkbox" id="showoriginalprice" name="showoriginalprice"
onChange="setShowOriginalPrice()" <?php if ($settings['show_original_price'])
echo 'checked'; ?>>
onChange="setShowOriginalPrice()" <?= $settings['show_original_price'] ? 'checked' : '' ?>>
<label for="showoriginalprice"><?= translate('show_original_price', $i18n) ?></label>
</div>
</div>
@@ -1172,8 +1256,7 @@ $userData['currency_symbol'] = $currencies[$main_currency]['symbol'];
<div>
<div class="form-group-inline">
<input type="checkbox" id="mobilenavigation" name="mobilenavigation"
onChange="setMobileNavigation()" <?php if ($settings['mobile_nav'])
echo 'checked'; ?>>
onChange="setMobileNavigation()" <?= $settings['mobile_nav'] ? 'checked' : '' ?>>
<label for="mobilenavigation"><?= translate('use_mobile_navigation_bar', $i18n) ?></label>
</div>
<div class="mobile-nav-image">
@@ -1182,8 +1265,7 @@ $userData['currency_symbol'] = $currencies[$main_currency]['symbol'];
<div>
<div class="form-group-inline">
<input type="checkbox" id="showsubscriptionprogress" name="showsubscriptionprogress"
onChange="setShowSubscriptionProgress()" <?php if ($settings['show_subscription_progress'])
echo 'checked'; ?>>
onChange="setShowSubscriptionProgress()" <?= $settings['show_subscription_progress'] ? 'checked' : '' ?>>
<label for="showsubscriptionprogress"><?= translate('show_subscription_progress', $i18n) ?></label>
</div>
</div>
@@ -1191,16 +1273,15 @@ $userData['currency_symbol'] = $currencies[$main_currency]['symbol'];
<div>
<div class="form-group-inline">
<input type="checkbox" id="disabledtobottom" name="disabledtobottom"
onChange="setDisabledToBottom()" <?php if ($settings['disabled_to_bottom'])
echo 'checked'; ?>>
onChange="setDisabledToBottom()" <?= $settings['disabled_to_bottom'] ? 'checked' : '' ?>>
<label
for="disabledtobottom"><?= translate('show_disabled_subscriptions_at_the_bottom', $i18n) ?></label>
</div>
</div>
<div>
<div class="form-group-inline">
<input type="checkbox" id="hidedisabled" name="hidedisabled" onChange="setHideDisabled()" <?php if ($settings['hide_disabled'])
echo 'checked'; ?>>
<input type="checkbox" id="hidedisabled" name="hidedisabled" onChange="setHideDisabled()"
<?= $settings['hide_disabled'] ? 'checked' : '' ?>>
<label for="hidedisabled"><?= translate('hide_disabled_subscriptions', $i18n) ?></label>
</div>
</div>
@@ -1215,8 +1296,7 @@ $userData['currency_symbol'] = $currencies[$main_currency]['symbol'];
<div>
<div class="form-group-inline">
<input type="checkbox" id="removebackground" name="removebackground"
onChange="setRemoveBackground()" <?php if ($settings['remove_background'])
echo 'checked'; ?>>
onChange="setRemoveBackground()" <?= $settings['remove_background'] ? 'checked' : '' ?>>
<label for="removebackground"><?= translate('remove_background', $i18n) ?></label>
</div>
</div>

View File

@@ -28,6 +28,10 @@ textarea {
font-weight: 400;
}
input.hidden {
display: none;
}
@media (max-width: 768px) {
body.no-scroll section.contain {
display: none;
@@ -961,6 +965,7 @@ header #avatar {
.account-members .buttons,
.account-currencies .buttons,
.account-fixer .buttons,
.account-ai-settings .buttons,
.account-categories .buttons,
.account-notifications .buttons,
.admin-form .buttons,
@@ -2541,6 +2546,16 @@ button.dark-theme-button i {
.mobile-grow {
flex-grow: 1;
}
.mobile-reverse {
flex-direction: column-reverse;
}
.mobile-grow-force {
flex-grow: 1;
width: 100%;
}
}
.bold {
@@ -3025,4 +3040,48 @@ input[type="radio"]:checked+label::after {
font-size: 16px;
font-weight: 600;
color: var(--accent-color);
}
.flex {
display: flex;
}
.info-badge {
background-color: orange;
border-radius: 5px;
font-size: 10px;
padding: 2px 6px;
color: #FFFFFF;
margin-bottom: auto;
margin-left: 10px;
}
.spinner {
width: 38px;
height: 38px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: var(--main-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: auto;
}
.spinner.ai-spinner {
margin: 0px 0px 0px auto;
}
.spinner.hidden {
display: none;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 768px) {
.spinner.ai-spinner {
margin: auto;
}
}