mirror of
https://github.com/ellite/Wallos.git
synced 2025-12-23 23:18:07 -05:00
feat: ai recommendations
This commit is contained in:
143
endpoints/ai/fetch_models.php
Normal file
143
endpoints/ai/fetch_models.php
Normal 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);
|
||||
}
|
||||
326
endpoints/ai/generate_recommendations.php
Normal file
326
endpoints/ai/generate_recommendations.php
Normal 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 user’s 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;
|
||||
}
|
||||
99
endpoints/ai/save_settings.php
Normal file
99
endpoints/ai/save_settings.php
Normal 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);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" => "Χρώματα",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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" => "色",
|
||||
|
||||
@@ -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" => "색상",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" => "Цвета",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" => "Боје",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" => "Кольори",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" => "颜色",
|
||||
|
||||
@@ -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" => "自訂顏色",
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
}
|
||||
104
settings.php
104
settings.php
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user