Add (initial): futurewalker code

This commit is contained in:
2023-11-20 00:15:18 +08:00
parent f8602cb456
commit 9ce3e5c82a
166 changed files with 15941 additions and 1072 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -11,7 +11,7 @@ LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=echoscoop
DB_DATABASE=FutureWalker
DB_USERNAME=root
DB_PASSWORD=

View File

@@ -2,11 +2,13 @@
namespace App\Console;
use App\Jobs\AISerpGenArticleJob;
use Carbon\Carbon;
use App\Jobs\BrowseAndWriteWithAIJob;
use App\Jobs\PublishIndexPostJob;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Models\Post;
class Kernel extends ConsoleKernel
{
/**
@@ -15,20 +17,17 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
// AI Gen Scheduler
$schedule->call(function () {
BrowseAndWriteWithAIJob::dispatch()->onQueue('default')->onConnection('default');
})->dailyAt('00:00');
// $launched_date = Carbon::parse(intval(config('platform.global.launched_epoch')));
// $days_since_launch = now()->diffInDays($launched_date) + 1;
// $posts_to_generate = get_exponential_posts_gen_by_day($days_since_launch);
// $mins_between_posts = floor((24 * 60) / $posts_to_generate);
$schedule->call(function () {
$future_post = Post::whereNotNull('published_at')->where('status','future')->where('published_at', '>=', now())->orderBy('published_at','ASC')->first();
// $schedule->call(function () {
// AISerpGenArticleJob::dispatch()->onQueue('default')->onConnection('default');
// })->everyMinute()->when(function () use ($mins_between_posts, $launched_date) {
// $minutes_since_launch = now()->diffInMinutes($launched_date);
PublishIndexPostJob::dispatch($future_post->id)->onQueue('default')->onConnection('default');
// return $minutes_since_launch % $mins_between_posts === 0;
// });
})->everyMinute();
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Helpers\FirstParty\DFS;
use Exception;
use Http;
class DFSSerp
{
public static function liveAdvanced($se, $se_type, $keyword, $location_name, $language_code, $depth, $search_param = null)
{
$api_url = config('dataforseo.url');
$api_version = config('dataforseo.api_version');
$api_timeout = config('dataforseo.timeout');
$query = [
'keyword' => $keyword,
'location_name' => $location_name,
'language_code' => $language_code,
];
if (!is_empty($search_param))
{
$query['search_param'] = $search_param;
}
try {
$response = Http::timeout($api_timeout)->withBasicAuth(config('dataforseo.login'), config('dataforseo.password'))->withBody(
json_encode([(object) $query])
)->post("{$api_url}{$api_version}serp/{$se}/{$se_type}/live/advanced");
if ($response->successful()) {
return $response->body();
}
} catch (Exception $e) {
return null;
}
return null;
}
}

View File

@@ -5,118 +5,184 @@
use Exception;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class OpenAI
{
public static function writeArticle($title, $description, $article_type, $min, $max)
public static function getArticleMeta($user_prompt, $model_max_tokens = 1536, $timeout = 60)
{
$system_prompt = "
Using the general article structure, please create a Markdown format article on the topic given. The article should prioritize accuracy and provide genuine value to readers. Avoid making assumptions or adding unverified facts. Ensure the article is between {$min}-{$max} words. Write with 8th & 9th grade US english standard.\n\n
Structure:\n\n
Title\n
Provide a headline that captures the essence of the article's focus and is tailored to the reader's needs.\n\n
Introduction\n
Offer a brief overview or background of the topic, ensuring it's engaging and invites readers to continue reading.\n\n
Main Body\n\n
Subsection\n
Introduce foundational information about the topic, ensuring content is accurate and based on known facts. Avoid generic or speculative statements.\n\n
Subsection (if applicable)\n
If helpful, use Markdown to create tables to convey comparison of data. Ensure data is accurate and relevant to the reader.\n\n
Subsection\n
Dive deep into primary elements or facets of the topic, ensuring content is factual and offers value.\n\n
Subsection\n
Discuss real-world applications or significance, highlighting practical implications or actionable insights for the reader.\n\n
Subsection (optional)\n
Provide context or relate the topic to relevant past events or trends, making it relatable and more comprehensive.\n\n
Subsection (if applicable)\n
Predict outcomes, trends, or ramifications, but ensure predictions are rooted in known information or logical reasoning.\n\n
Subsection\n
Summarise key points or lessons, ensuring they resonate with the initial objectives of the article and provide clear takeaways.\n\n
Conclusion\n
Revisit main points and offer final thoughts or recommendations that are actionable and beneficial to the reader.\n\n
FAQs (optional)\n
Address anticipated questions or misconceptions about the topic. Prioritize questions that readers are most likely to have and provide clear, concise answers based on factual information.\n
Q: Question\n\n
A: Answer\n
";
$user_prompt = "Title: {$title}\nDescription: {$description}\nArticleType: {$article_type}";
$openai_config = 'openai-gpt-4-turbo';
return self::chatCompletion($system_prompt, $user_prompt, 'gpt-3.5-turbo', 1200);
$system_prompt = "Based on given article, populate the following in valid JSON format\n{\n\"title\":\"(Title based on article)\",\n\"keywords\":[\"(Important keywords in 1-2 words per keyword)\"],\n\"category\":\"(Updates|Opinions|Features|New Launches|How Tos|Reviews)\",\n\"summary\":\"(Summarise article in 80-100 words to help readers understand what article is about)\",\n\"entities\":[(List of companies, brands that are considered as main entites in 1-2 words. per entity)],\n\"society_impact\":[\"(Explain how this article content's can impact society on AI and\/or technology aspect )\"],\n\"society_impact_level:\"(low|medium|high)\"\n}";
return self::getChatCompletion($user_prompt, $system_prompt, $openai_config, $model_max_tokens, $timeout);
}
public static function createNewArticleTitle($current_title, $supporting_data)
public static function writeArticle($user_prompt, $model_max_tokens = 1536, $timeout = 180)
{
$system_prompt = "Based on provided article title, identify the main keyword in 1-2 words. Once identified, use the main keyword only to generate an easy-to-read unique, helpful article title.\n\n
Requirements:\n
2 descriptive photos keywords to represent article title when put together side-by-side\n
No realtime information required\n
No guides and how tos\n
No punctuation in titles especially colons :\n
90-130 characters\n\n
return in following json format {\"main_keyword\":\"(Main Keyword)\",\"title\":\"(Title in 90-130 letters)\",\"short_title\":\"(Short Title in 30-40 letters)\",\"article_type\":\"(How-tos|Guides|Interview|Review|Commentary|Feature|News|Editorial|Report|Research|Case-study|Overview|Tutorial|Update|Spotlight|Insights)\",\"description\":\"(Summarize in max 120 letters, add cliffhanger is possible to attract readers)\",\"photo_keywords\":[\"photo keyword 1\",\"photo keyword 2\"]}";
$openai_config = 'openai-gpt-3-5-turbo-1106';
$supporting_data = Str::substr($supporting_data, 0, 2100);
$system_prompt = "Write a news article in US grade 9 English, approximately 600-800 words, formatted in Markdown. \n\nIMPORTANT RULES\n- Do not add photos, publish date, or author\n- Only have 1 heading, which is the article title\n- Write in the following article structure:\n# Main article title\n\nParagraph 1\n\nParagraph 2\n\nParagraph 3, etc.\n\nConclusion";
$user_prompt = "Article Title: {$current_title}\n Article Description: {$supporting_data}\n";
$reply = self::chatCompletion($system_prompt, $user_prompt, 'gpt-3.5-turbo', 900);
try {
return json_decode($reply, false);
} catch (Exception $e) {
return null;
}
return self::getChatCompletion($user_prompt, $system_prompt, $openai_config, $model_max_tokens, $timeout, 'text');
}
public static function suggestArticleTitles($current_title, $supporting_data, $suggestion_counts)
public static function titleSuggestions($user_prompt, $model_max_tokens = 512, $timeout = 60)
{
$system_prompt = "Based on provided article title, identify the main keyword in 1-2 words. Once identified, use the main keyword only to generate {$suggestion_counts} easy-to-read unique, helpful title articles.\n\n
Requirements:\n
2 descriptive photos keywords to represent article title when put together side-by-side\n
No realtime information required\n
No guides and how tos\n
No punctuation in titles especially colons :\n
90-130 characters\n\n
return in following json format {\"main_keyword\":\"(Main Keyword)\",\"suggestions\":[{\"title\":\"(Title in 90-130 letters)\",\"short_title\":\"(Short Title in 30-40 letters)\",\"article_type\":\"(How-tos|Guides|Interview|Review|Commentary|Feature|News|Editorial|Report|Research|Case-study|Overview|Tutorial|Update|Spotlight|Insights)\",\"description\":\"(SEO description based on main keyword)\",\"photo_keywords\":[\"photo keyword 1\",\"photo keyword 2\"]}]}";
$openai_config = 'openai-gpt-3-5-turbo-1106';
$user_prompt = "Article Title: {$current_title}\n Article Description: {$supporting_data}\n";
$system_prompt = "1. identify meaningful & potential keywords in this blog post article title. also estimate other related keywords to the title.\n\n2. using identify keywords, propose search queries i can use to find relevant articles online\n\n3. recommend writing tone that will entice readers.\n\n4. using identified keywords, propose article headings with key facts to highlight for this article, without reviews\n\n\nreturn all content in json: {\n\"identified_keywords\":[],\n\"related_keywords\":[],\n\"proposed_search_queries\":[],\n\"writing_tone\":[],\n\"article_headings\":[],\n}";
$reply = self::chatCompletion($system_prompt, $user_prompt, 'gpt-3.5-turbo');
return self::getChatCompletion($user_prompt, $system_prompt, $openai_config, $model_max_tokens, $timeout);
}
public static function topTitlePicksById($user_prompt, $model_max_tokens = 256, $timeout = 60)
{
$openai_config = 'openai-gpt-4-turbo';
$system_prompt = 'Pick 10-15 unique articles that are focused on different product launches, ensuring each is interesting, informative, and casts a positive light on the technology and AI industry. Avoid selecting multiple articles that center around the same product or feature. Ensure that titles selected do not share primary keywords—such as the name of a product or specific technology feature—and strictly return a list of IDs only, without title, strictly in this JSON format: {"ids":[]}. Titles should represent a diverse range of topics and products within the technology and AI space without repetition.';
return self::getChatCompletion($user_prompt, $system_prompt, $openai_config, $model_max_tokens, $timeout = 800);
}
private static function getChatCompletion($user_prompt, $system_prompt, $openai_config, $model_max_tokens, $timeout, $response_format = 'json_object')
{
$model = config("platform.ai.{$openai_config}.model");
$input_cost_per_thousand_tokens = config("platform.ai.{$openai_config}.input_cost_per_thousand_tokens");
$output_cost_per_thousand_tokens = config("platform.ai.{$openai_config}.output_cost_per_thousand_tokens");
$output_token = 1280;
try {
return json_decode($reply, false);
$obj = self::chatCompletionApi($system_prompt, $user_prompt, $model, $output_token, $response_format, $timeout);
$input_cost = self::getCostUsage($obj->usage_detailed->prompt_tokens, $input_cost_per_thousand_tokens);
$output_cost = self::getCostUsage($obj->usage_detailed->completion_tokens, $output_cost_per_thousand_tokens);
$output = $obj->reply;
if ($response_format == 'json_object') {
$output = json_decode(self::jsonFixer($obj->reply), false, 512, JSON_THROW_ON_ERROR);
}
return (object) [
'prompts' => (object) [
'system_prompt' => $system_prompt,
'user_prompt' => $user_prompt,
],
'cost' => $input_cost + $output_cost,
'output' => $output,
'token_usage' => $obj->usage,
'token_usage_detailed' => $obj->usage_detailed,
];
} catch (Exception $e) {
return null;
return self::getDefaultFailedResponse($system_prompt, $user_prompt, $e);
}
return self::getDefaultFailedResponse($system_prompt, $user_prompt);
}
public static function chatCompletion($system_prompt, $user_prompt, $model, $max_token = 2500)
private static function getDefaultFailedResponse($system_prompt, $user_prompt, $exception = null)
{
$exception_message = null;
if (! is_null($exception)) {
$exception_message = $exception->getMessage();
}
return (object) [
'exception' => $exception_message,
'prompts' => (object) [
'system_prompt' => $system_prompt,
'user_prompt' => $user_prompt,
],
'cost' => 0,
'output' => null,
'token_usage' => 0,
'token_usage_detailed' => (object) [
'completion_tokens' => 0,
'prompt_tokens' => 0,
'total_tokens' => 0,
],
];
}
private static function getCostUsage($token_usage, $cost_per_thousand_tokens)
{
$calc = $token_usage / 1000;
return $calc * $cost_per_thousand_tokens;
}
private static function jsonFixer($json_string)
{
$json_string = str_replace("\n", '', $json_string);
// try {
// return (new JsonFixer)->fix($json_string);
// }
// catch(Exception $e) {
// }
return $json_string;
}
public static function chatCompletionApi($system_prompt, $user_prompt, $model, $max_token = 2500, $response_format = 'text', $timeout = 800)
{
if ($response_format == 'json_object') {
$arr = [
'model' => $model,
'max_tokens' => $max_token,
'response_format' => (object) [
'type' => 'json_object',
],
'messages' => [
['role' => 'system', 'content' => $system_prompt],
['role' => 'user', 'content' => $user_prompt],
],
];
} else {
$arr = [
'model' => $model,
'max_tokens' => $max_token,
'messages' => [
['role' => 'system', 'content' => $system_prompt],
['role' => 'user', 'content' => $user_prompt],
],
];
}
try {
$response = Http::timeout(800)->withToken(config('platform.ai.openai.api_key'))
->post('https://api.openai.com/v1/chat/completions', [
'model' => $model,
'max_tokens' => $max_token,
'messages' => [
['role' => 'system', 'content' => $system_prompt],
['role' => 'user', 'content' => $user_prompt],
],
]);
$response = Http::timeout($timeout)->withToken(config('platform.ai.openai.api_key'))
->post('https://api.openai.com/v1/chat/completions', $arr);
$json_response = json_decode($response->body());
$reply = $json_response?->choices[0]?->message?->content;
//dump($json_response);
return $reply;
if (isset($json_response->error)) {
Log::error(serialize($json_response));
throw new Exception(serialize($json_response->error));
}
$obj = (object) [
'usage' => $json_response?->usage?->total_tokens,
'usage_detailed' => $json_response?->usage,
'reply' => $json_response?->choices[0]?->message?->content,
];
return $obj;
} catch (Exception $e) {
Log::error($response->body());
inspector()->reportException($e);
////dd($response->body());
//inspector()->reportException($e);
throw ($e);
}

View File

@@ -3,3 +3,4 @@
require 'string_helper.php';
require 'geo_helper.php';
require 'platform_helper.php';
require 'proxy_helper.php';

View File

@@ -31,3 +31,19 @@ function get_exponential_posts_gen_by_day($day, $period_days = 45)
return $value;
}
}
if (! function_exists('get_notification_channel')) {
function get_notification_channel()
{
return 'telegram';
}
}
if (! function_exists('get_notification_user_id')) {
function get_notification_user_id()
{
return '629991336';
}
}

View File

@@ -0,0 +1,67 @@
<?php
if (! function_exists('get_smartproxy_rotating_server')) {
function get_smartproxy_rotating_server()
{
$proxy = config('platform.proxy.smartproxy.rotating_global.server');
$proxy_user = config('platform.proxy.smartproxy.rotating_global.user');
$proxy_psw = config('platform.proxy.smartproxy.rotating_global.password');
$reproxy_enable = config('platform.proxy.smartproxy.rotating_global.reproxy_enable');
if ($reproxy_enable) {
$proxy = config('platform.proxy.smartproxy.rotating_global.reproxy');
}
$proxy_server = "$proxy_user:$proxy_psw@$proxy";
return $proxy_server;
}
}
if (! function_exists('get_smartproxy_unblocker_server')) {
function get_smartproxy_unblocker_server()
{
$proxy = config('platform.proxy.smartproxy.unblocker.server');
$proxy_user = config('platform.proxy.smartproxy.unblocker.user');
$proxy_psw = config('platform.proxy.smartproxy.unblocker.password');
$reproxy_enable = config('platform.proxy.smartproxy.unblocker.reproxy_enable');
if ($reproxy_enable) {
$proxy = config('platform.proxy.smartproxy.unblocker.reproxy');
}
$proxy_server = "$proxy_user:$proxy_psw@$proxy";
return $proxy_server;
}
}
if (! function_exists('get_smartproxy_server')) {
function get_smartproxy_server()
{
$proxy = config('platform.proxy.smartproxy.rotating_global.server');
$proxy_user = config('platform.proxy.smartproxy.rotating_global.user');
$proxy_psw = config('platform.proxy.smartproxy.rotating_global.password');
$reproxy_enable = config('platform.proxy.smartproxy.rotating_global.reproxy_enable');
if ($reproxy_enable) {
$proxy = config('platform.proxy.smartproxy.rotating_global.reproxy');
}
$proxy_server = "$proxy_user:$proxy_psw@$proxy";
return $proxy_server;
}
}
if (! function_exists('calculate_smartproxy_cost')) {
function calculate_smartproxy_cost(float $kb, $type = 'rotating_global')
{
$cost_per_gb = config("platform.proxy.smartproxy.{$type}.cost_per_gb");
// Convert cost per GB to cost per KB
$cost_per_kb = $cost_per_gb / (1024 * 1024);
// Calculate total cost for the given $kb
$cost = ($cost_per_kb * $kb);
return round($cost, 3);
}
}

View File

@@ -3,6 +3,24 @@
use GrahamCampbell\Markdown\Facades\Markdown;
use Illuminate\Support\Str;
if (! function_exists('is_valid_url')) {
function is_valid_url($url)
{
// Check if the URL is a valid full URL
if (filter_var($url, FILTER_VALIDATE_URL)) {
return true;
}
// Check if it's a valid relative URL
// Modify this regex as needed for your specific URL formats
if (preg_match('/^\/[\w\/_.-]+(\.[\w]+)?$/i', $url)) {
return true;
}
return false;
}
}
if (! function_exists('epoch_now_timestamp')) {
function epoch_now_timestamp()
{
@@ -17,6 +35,14 @@ function read_duration($text)
}
}
if (! function_exists('remove_newline')) {
function remove_newline($string)
{
return preg_replace('/\s+/', ' ', trim($string));
}
}
if (! function_exists('plain_text')) {
function plain_text($content)
{
@@ -24,6 +50,32 @@ function plain_text($content)
}
}
if (! function_exists('markdown_to_plaintext')) {
function markdown_to_plaintext($markdown)
{
// Headers
$markdown = preg_replace('/^#.*$/m', '', $markdown);
// Links and images
$markdown = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $markdown);
// Bold and italic
$markdown = str_replace(['**', '__', '*', '_'], '', $markdown);
// Ordered and unordered lists
$markdown = preg_replace('/^[-\*].*$/m', '', $markdown);
// Horizontal line
$markdown = str_replace(['---', '***', '- - -', '* * *'], '', $markdown);
// Inline code and code blocks
$markdown = preg_replace('/`.*?`/', '', $markdown);
$markdown = preg_replace('/```[\s\S]*?```/', '', $markdown);
// Blockquotes
$markdown = preg_replace('/^>.*$/m', '', $markdown);
// Remove multiple spaces, leading and trailing spaces
$plaintext = trim(preg_replace('/\s+/', ' ', $markdown));
return $plaintext;
}
}
if (! function_exists('markdown_min_read')) {
function markdown_min_read($markdown)
{
@@ -48,12 +100,12 @@ function str_slug($string, $delimiter = '-')
if (! function_exists('str_first_sentence')) {
function str_first_sentence($str)
{
// Split the string at ., !, or ?
$sentences = preg_split('/(\.|!|\?)(\s|$)/', $str, 2);
// Split the string at ., !, or ? but include these characters in the match
$sentences = preg_split('/([.!?])\s/', $str, 2, PREG_SPLIT_DELIM_CAPTURE);
// Return the first part of the array if available
if (isset($sentences[0])) {
return trim($sentences[0]);
// Check if we have captured the first sentence and its punctuation
if (isset($sentences[0]) && isset($sentences[1])) {
return trim($sentences[0].$sentences[1]);
}
// If no sentence ending found, return the whole string

View File

@@ -13,12 +13,18 @@ class FrontHomeController extends Controller
{
public function home(Request $request)
{
$featured_post = Post::where('status', 'publish')->orderBy('published_at', 'desc')->first();
$latest_posts = Post::where(function ($query) use ($featured_post) {
$query->whereNotIn('id', [$featured_post?->id]);
})->where('status', 'publish')->orderBy('published_at', 'desc')->limit(5)->get();
// $featured_post = Post::where('status', 'publish')->orderBy('published_at', 'desc')->first();
// $latest_posts = Post::where(function ($query) use ($featured_post) {
// $query->whereNotIn('id', [$featured_post?->id]);
// })->where('status', 'publish')->orderBy('published_at', 'desc')->limit(5)->get();
return response(view('front.welcome', compact('featured_post', 'latest_posts')), 200);
$featured_posts = Post::where('status', 'publish')->where('published_at', '<=', now())->orderBy('published_at', 'desc')->limit(6)->get();
$latest_posts = Post::where(function ($query) use ($featured_posts) {
$query->whereNotIn('id', $featured_posts->pluck('id')->toArray());
})->where('status', 'publish')->where('published_at', '<=', now())->orderBy('published_at', 'desc')->limit(6)->get();
return response(view('front.welcome', compact('featured_posts', 'latest_posts')), 200);
}
public function terms(Request $request)
@@ -70,7 +76,7 @@ public function disclaimer(Request $request)
$markdown = file_get_contents(resource_path('markdown/disclaimer.md'));
$title = 'Disclaimer';
$description = 'EchoScoop provides the content on this website purely for informational purposes and should not be interpreted as legal, financial, or medical guidance.';
$description = 'FutureWalker provides the content on this website purely for informational purposes and should not be interpreted as legal, financial, or medical guidance.';
SEOTools::metatags();
SEOTools::twitter();

View File

@@ -12,14 +12,65 @@
class FrontListController extends Controller
{
public function search(Request $request)
{
$page_type = 'search';
$query = $request->get('query', '');
$breadcrumbs = collect([
['name' => 'Home', 'url' => route('front.home')],
['name' => 'Search', 'url' => null],
['name' => $query, 'url' => url()->current()],
]);
$title = 'Latest News about ' . ucwords($query) . ' in FutureWalker';
SEOTools::metatags();
SEOTools::twitter();
SEOTools::opengraph();
SEOTools::jsonLd();
SEOTools::setTitle($title, false);
// Use full-text search capabilities of your database
// For example, using MySQL's full-text search with MATCH...AGAINST
$posts = Post::with('category')
->where('status', 'publish')
->whereRaw("to_tsvector('english', title || ' ' || bites) @@ to_tsquery('english', ?)", [$query])
->orderBy('published_at', 'desc')
->cursorPaginate(10);
// breadcrumb json ld
$listItems = [];
foreach ($breadcrumbs as $index => $breadcrumb) {
$listItems[] = [
'name' => $breadcrumb['name'],
'url' => $breadcrumb['url'],
];
}
$breadcrumb_context = Context::create('breadcrumb_list', [
'itemListElement' => $listItems,
]);
return view('front.post_list', compact('posts', 'breadcrumbs', 'breadcrumb_context', 'title','page_type'));
}
public function index(Request $request)
{
$page_type = 'default';
$breadcrumbs = collect([
['name' => 'Home', 'url' => route('front.home')],
['name' => 'Latest News', 'url' => null], // or you can set a route for Latest News if there's a specific one
]);
$title = 'Latest News from EchoScoop';
$title = 'Latest News from FutureWalker';
SEOTools::metatags();
SEOTools::twitter();
@@ -27,7 +78,7 @@ public function index(Request $request)
SEOTools::jsonLd();
SEOTools::setTitle($title, false);
$posts = Post::where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10) ?? collect();
$posts = Post::with('category')->where('status', 'publish')->orderBy('published_at', 'desc')->cursorPaginate(10) ?? collect();
// breadcrumb json ld
$listItems = [];
@@ -39,15 +90,19 @@ public function index(Request $request)
];
}
//dd($posts);
$breadcrumb_context = Context::create('breadcrumb_list', [
'itemListElement' => $listItems,
]);
return view('front.post_list', compact('posts', 'breadcrumbs', 'breadcrumb_context'));
return view('front.post_list', compact('posts', 'breadcrumbs', 'breadcrumb_context','page_type'));
}
public function category(Request $request, $category_slug)
{
$page_type = 'default';
// Fetch the category by slug
$category = Category::where('slug', $category_slug)->first();
@@ -68,9 +123,9 @@ public function category(Request $request, $category_slug)
// Get the posts associated with these category IDs
$postIds = PostCategory::whereIn('category_id', $categoryIds)->pluck('post_id');
$posts = Post::whereIn('id', $postIds)->where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10);
$posts = Post::whereIn('id', $postIds)->where('status', 'publish')->orderBy('published_at', 'desc')->cursorPaginate(10);
$title = $category->name.' News from EchoScoop';
$title = $category->name.' News from FutureWalker';
SEOTools::metatags();
SEOTools::twitter();
@@ -92,6 +147,6 @@ public function category(Request $request, $category_slug)
'itemListElement' => $listItems,
]);
return view('front.post_list', compact('category', 'posts', 'breadcrumbs', 'breadcrumb_context'));
return view('front.post_list', compact('category', 'posts', 'breadcrumbs', 'breadcrumb_context','page_type'));
}
}

View File

@@ -42,7 +42,7 @@ public function index(Request $request, $category_slug, $slug)
$content = $this->injectFeaturedImage($post, $content);
$content = $this->injectPublishDateAndAuthor($post, $content);
$post_description = $post->excerpt.' '.$post->title.' by EchoScoop.';
$post_description = $post->excerpt.' '.$post->title.' by FutureWalker.';
// Generate breadcrumb data
$breadcrumbs = collect([
@@ -71,7 +71,6 @@ public function index(Request $request, $category_slug, $slug)
SEOMeta::setTitle($post->title, false);
SEOMeta::setDescription($post_description);
SEOMeta::addMeta('article:published_time', $post->published_at->format('Y-m-d'), 'property');
SEOMeta::addMeta('article:section', $post->category->name, 'property');
SEOMeta::setRobots('INDEX, FOLLOW, MAX-IMAGE-PREVIEW:LARGE, MAX-SNIPPET:-1, MAX-VIDEO-PREVIEW:-1');
OpenGraph::setDescription($post_description);
@@ -89,15 +88,15 @@ public function index(Request $request, $category_slug, $slug)
->addImage($post->featured_image_cdn)
->addValue('author', [
'type' => 'Person',
'name' => $post->author->name,
'name' => 'FutureWalker',
'url' => config('app.url'),
])
->addValue('publisher', [
'type' => 'Organization',
'name' => 'EchoScoop',
'name' => 'FutureWalker',
'logo' => [
'type' => 'ImageObject',
'url' => asset('echoscoop-logo-512x512.png'),
'url' => asset('FutureWalker-logo-512x512.png'),
],
])
->addValue('datePublished', $post->published_at->format('Y-m-d'))
@@ -231,7 +230,7 @@ private function injectTableOfContents($html)
private function injectPublishDateAndAuthor($post, $content)
{
$publishedAtFormatted = $post->published_at->format('F j, Y');
$authorName = $post->author->name;
$authorName = 'FutureWalker Team';
// Create the HTML structure for publish date and author
$publishInfo = "<div class=\"mb-4\"><span class=\"text-secondary small\">Published on {$publishedAtFormatted} by {$authorName}<i class=\"bi bi-dot\"></i>".markdown_min_read($post->body).'</span></div>';

View File

@@ -17,6 +17,8 @@ class AISerpGenArticleJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 600;
/**
* Create a new job instance.
*/

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Jobs;
use App\Jobs\Tasks\BrowseDFSLatestNewsTask;
use App\Jobs\Tasks\ParseDFSNewsTask;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class BrowseAndWriteWithAIJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 20;
/**
* Create a new job instance.
*/
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
$news_serp_result = BrowseDFSLatestNewsTask::handle('ai', 'US');
if (! is_null($news_serp_result)) {
ParseDFSNewsTask::handle($news_serp_result);
} else {
throw new Exception('Empty news_serp_result. API error?');
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Jobs;
use App\Jobs\Tasks\BrowseDFSForResearchTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class BrowseDFSForResearchJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $serp_url_id;
public $timeout = 60;
/**
* Create a new job instance.
*/
public function __construct(int $serp_url_id)
{
$this->serp_url_id = $serp_url_id;
}
/**
* Execute the job.
*/
public function handle(): void
{
BrowseDFSForResearchTask::handle($this->serp_url_id);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Jobs;
use App\Jobs\Tasks\CrawlUrlResearchTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CrawlUrlResearchJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $serp_url_research_id;
public $timeout = 15;
/**
* Create a new job instance.
*/
public function __construct($serp_url_research_id)
{
$this->serp_url_research_id = $serp_url_research_id;
}
/**
* Execute the job.
*/
public function handle(): void
{
if (! is_null($this->serp_url_research_id)) {
CrawlUrlResearchTask::handle($this->serp_url_research_id);
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Jobs;
use App\Jobs\Tasks\FillPostMetadataTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class FillPostMetadataJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $post_id;
public $timeout = 240;
/**
* Create a new job instance.
*/
public function __construct(int $post_id)
{
$this->post_id = $post_id;
}
/**
* Execute the job.
*/
public function handle(): void
{
FillPostMetadataTask::handle($this->post_id);
}
}

View File

@@ -15,7 +15,7 @@ class GenerateArticleJob implements ShouldQueue
protected $serp_url;
public $timeout = 600;
public $timeout = 240;
/**
* Create a new job instance.

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Jobs;
use App\Jobs\Tasks\IdentifyCrawlSourcesTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class IdentifyCrawlSourcesJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $serp_url_id;
public $timeout = 120;
/**
* Create a new job instance.
*/
public function __construct(int $serp_url_id)
{
$this->serp_url_id = $serp_url_id;
}
/**
* Execute the job.
*/
public function handle(): void
{
IdentifyCrawlSourcesTask::handle($this->serp_url_id);
}
}

View File

@@ -14,14 +14,16 @@ class PublishIndexPostJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $post;
protected $post_id;
public $timeout = 10;
/**
* Create a new job instance.
*/
public function __construct(Post $post)
public function __construct(int $post_id)
{
$this->post = $post;
$this->post_id = $post_id;
}
/**
@@ -29,8 +31,6 @@ public function __construct(Post $post)
*/
public function handle(): void
{
if (! is_null($this->post)) {
PublishIndexPostTask::handle($this->post);
}
PublishIndexPostTask::handle($this->post_id);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Jobs;
use App\Jobs\Tasks\SchedulePublishTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SchedulePublishPost implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $post_id;
protected $status;
public $timeout = 30;
/**
* Create a new job instance.
*/
public function __construct($post_id, $status)
{
$this->post_id = $post_id;
$this->status = $status;
}
/**
* Execute the job.
*/
public function handle(): void
{
SchedulePublishTask::handle($this->post_id, $this->status);
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Jobs\Tasks;
use App\Helpers\FirstParty\DFS\DFSSerp;
use App\Helpers\FirstParty\OpenAI\OpenAI;
use App\Helpers\ThirdParty\DFS\SettingSerpLiveAdvanced;
use App\Jobs\CrawlUrlResearchJob;
use App\Models\SerpUrl;
use App\Models\SerpUrlResearch;
use App\Models\ServiceCostUsage;
use Exception;
class BrowseDFSForResearchTask
{
public static function handle(int $serp_url_id)
{
$serp_url = SerpUrl::find($serp_url_id);
if ((! is_null($serp_url)) && (! is_null($serp_url->suggestion_data))) {
if (isset($serp_url->suggestion_data->proposed_search_queries)) {
if (count($serp_url->suggestion_data->proposed_search_queries) > 0) {
$search_query = $serp_url->suggestion_data->proposed_search_queries[0];
// $serp_model = new SettingSerpLiveAdvanced;
// $serp_model->setSe('google');
// $serp_model->setSeType('organic');
// $serp_model->setKeyword(strtolower($search_query));
// $serp_model->setLocationName('United States');
// //$serp_model->setDepth(100);
// $serp_model->setLanguageCode('en');
// $serp_res = $serp_model->getAsJson();
// print_r($serp_res);
// die();
$country_name = get_country_name_by_iso($serp_url->country_iso);
$serp_res = DFSSerp::liveAdvanced('google', 'news', $search_query, $country_name, 'en', 100);
try {
$serp_obj = json_decode($serp_res, false, 512, JSON_THROW_ON_ERROR);
if ($serp_obj?->status_code == 20000) {
$service_cost_usage = new ServiceCostUsage;
$service_cost_usage->cost = $serp_obj->cost;
$service_cost_usage->name = 'dataforseo-GoogleSerpApiAdvancedLiveOrganic';
$service_cost_usage->reference_1 = 'google';
$service_cost_usage->reference_2 = 'organic';
$service_cost_usage->output = $serp_obj;
$service_cost_usage->input_1 = $country_name;
$service_cost_usage->input_2 = $search_query;
$service_cost_usage->save();
$results = $serp_obj?->tasks[0]->result[0]?->items;
//$results = $serp_obj?->result[0]?->items;
// dump($serp_obj);
// exit();
$saved_count = 0;
$first_serp_url_research = null;
foreach ($results as $key => $result) {
if ($result->type == 'news_search') {
$serp_url_research = SerpUrlResearch::where('url', $result->url)->where('serp_url_id', $serp_url_id)->first();
if (is_null($serp_url_research)) {
//dump($result->url);
$serp_url_research = new SerpUrlResearch;
$serp_url_research->serp_url_id = $serp_url_id;
$serp_url_research->url = $result->url;
$serp_url_research->query = $search_query;
$serp_url_research->content = null;
if ($serp_url_research->save()) {
$saved_count++;
}
}
}
if ($saved_count >= 10) {
break;
}
}
$first_serp_url_research = SerpUrlResearch::where('serp_url_id', $serp_url_id)->orderBy('created_at', 'ASC')->whereNull('content')->first();
CrawlUrlResearchJob::dispatch($first_serp_url_research->id)->onQueue('default')->onConnection('default');
}
} catch (Exception $e) {
throw $e;
}
}
}
}
}
}
// {
// "identified_keywords":[
// "Humane AI Pin",
// "costs",
// "OpenAI",
// "T-Mobile integration"
// ],
// "related_keywords":[
// "artificial intelligence device",
// "monthly subscription",
// "OpenAI partnership",
// "T-Mobile collaboration"
// ],
// "proposed_search_queries":[
// "Humane AI Pin features",
// "Cost of Humane AI Pin",
// "Humane AI Pin integration with OpenAI and T-Mobile",
// "Reviews of Humane AI Pin"
// ],
// "writing_tone":[
// "engaging",
// "informative"
// ],
// "article_headings":[
// "Introduction to Humane AI Pin",
// "Features of Humane AI Pin",
// "Cost and Subscription Details",
// "OpenAI and T-Mobile Integration"
// ]
// }

View File

@@ -2,46 +2,59 @@
namespace App\Jobs\Tasks;
use App\Helpers\FirstParty\DFS\DFSSerp;
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
use App\Helpers\ThirdParty\DFS\SettingSerpLiveAdvanced;
use App\Models\Category;
use App\Models\NewsSerpResult;
use App\Models\ServiceCostUsage;
use DFSClientV3\DFSClient;
use Exception;
use Illuminate\Support\Facades\Log;
class GetNewsSerpTask
class BrowseDFSLatestNewsTask
{
public static function handle(Category $category, $country_iso)
public static function handle(string $keyword, $country_iso)
{
$country_name = get_country_name_by_iso($country_iso);
$keyword = strtolower("{$category->name}");
// $client = new DFSClient(
// config('dataforseo.login'),
// config('dataforseo.password'),
// config('dataforseo.timeout'),
// config('dataforseo.api_version'),
// config('dataforseo.url'),
// );
$client = new DFSClient(
config('dataforseo.login'),
config('dataforseo.password'),
config('dataforseo.timeout'),
config('dataforseo.api_version'),
config('dataforseo.url'),
);
// // You will receive SERP data specific to the indicated keyword, search engine, and location parameters
// $serp_model = new SettingSerpLiveAdvanced();
// You will receive SERP data specific to the indicated keyword, search engine, and location parameters
$serp_model = new SettingSerpLiveAdvanced();
// $serp_model->setSe('google');
// $serp_model->setSeType('news');
// $serp_model->setSearchParam('&tbs=qdr:d');
// $serp_model->setKeyword($keyword);
// $serp_model->setLocationName($country_name);
// $serp_model->setDepth(100);
// $serp_model->setLanguageCode('en');
// $serp_res = $serp_model->getAsJson();
$serp_model->setSe('google');
$serp_model->setSeType('news');
$serp_model->setKeyword($keyword);
$serp_model->setLocationName($country_name);
$serp_model->setDepth(100);
$serp_model->setLanguageCode('en');
$serp_res = $serp_model->getAsJson();
$serp_res = DFSSerp::liveAdvanced('google', 'news', $keyword, $country_name, 'en', 100, '&tbs=qdr:d');
try {
$serp_obj = json_decode($serp_res, false, 512, JSON_THROW_ON_ERROR);
if ($serp_obj?->status_code == 20000) {
$json_file_name = config('platform.dataset.news.news_serp.file_prefix').str_slug($category->name).'-'.epoch_now_timestamp().'.json';
$service_cost_usage = new ServiceCostUsage;
$service_cost_usage->cost = $serp_obj->cost;
$service_cost_usage->name = 'dataforseo-GoogleSerpApiAdvancedLiveNews';
$service_cost_usage->reference_1 = 'google';
$service_cost_usage->reference_2 = 'news';
$service_cost_usage->output = $serp_obj;
$service_cost_usage->input_1 = $country_name;
$service_cost_usage->input_2 = $keyword;
$service_cost_usage->save();
$json_file_name = config('platform.dataset.news.news_serp.file_prefix').str_slug($keyword).'-'.epoch_now_timestamp().'.json';
$upload_status = OSSUploader::uploadJson(
config('platform.dataset.news.news_serp.driver'),
@@ -50,9 +63,8 @@ public static function handle(Category $category, $country_iso)
$serp_obj);
if ($upload_status) {
$news_serp_result = new NewsSerpResult;
$news_serp_result->category_id = $category->id;
$news_serp_result->category_name = $category->name;
$news_serp_result->serp_provider = 'dfs';
$news_serp_result->serp_se = 'google';
$news_serp_result->serp_se_type = 'news';
@@ -62,10 +74,7 @@ public static function handle(Category $category, $country_iso)
$news_serp_result->result_count = $serp_obj?->tasks[0]?->result[0]?->items_count;
$news_serp_result->filename = $json_file_name;
$news_serp_result->status = 'initial';
if ($news_serp_result->save()) {
$category->serp_at = now();
$category->save();
}
$news_serp_result->save();
return $news_serp_result;
} else {

View File

@@ -0,0 +1,207 @@
<?php
namespace App\Jobs\Tasks;
use App\Jobs\CrawlUrlResearchJob;
use App\Jobs\WriteWithAIJob;
use App\Models\SerpUrl;
use App\Models\SerpUrlResearch;
use Exception;
use fivefilters\Readability\Configuration as ReadabilityConfiguration;
use fivefilters\Readability\ParseException as ReadabilityParseException;
use fivefilters\Readability\Readability;
use Illuminate\Support\Facades\Http;
use League\HTMLToMarkdown\HtmlConverter;
use Symfony\Component\DomCrawler\Crawler;
class CrawlUrlResearchTask
{
public static function handle(int $serp_url_research_id)
{
$serp_url_research = SerpUrlResearch::find($serp_url_research_id);
if (is_null($serp_url_research)) {
return null;
}
try {
$user_agent = config('platform.proxy.user_agent');
$response = Http::withHeaders([
'User-Agent' => $user_agent,
])
->withOptions([
'proxy' => get_smartproxy_rotating_server(),
'timeout' => 10,
'verify' => false,
])
->get($serp_url_research->url);
if ($response->successful()) {
$raw_html = $response->body();
$costs['unblocker'] = calculate_smartproxy_cost(round(strlen($raw_html) / 1024, 2), 'rotating_global');
} else {
$raw_html = null;
$response->throw();
}
} catch (Exception $e) {
$raw_html = null;
//throw $e;
}
if (! is_empty($raw_html)) {
//dump(self::getMarkdownFromHtml($raw_html));
$serp_url_research->content = self::getMarkdownFromHtml($raw_html);
$serp_url_research->main_image = self::getMainImageFromHtml($raw_html);
//dump($serp_url_research->content);
} else {
$serp_url_research->content = 'EMPTY CONTENT';
}
$serp_url_research->save();
$completed_serp_url_researches_counts = SerpUrlResearch::where('serp_url_id', $serp_url_research->serp_url_id)->where('content', '!=', 'EMPTY CONTENT')->whereNotNull('content')->count();
if ($completed_serp_url_researches_counts >= 3) {
$serp_url = SerpUrl::find($serp_url_research->serp_url_id);
if (! is_null($serp_url)) {
$serp_url->crawled = true;
$serp_url->save();
WriteWithAIJob::dispatch($serp_url->id)->onQueue('default')->onConnection('default');
}
} else {
$next_serp_url_research = SerpUrlResearch::where('serp_url_id', $serp_url_research->serp_url_id)->whereNull('content')->first();
if (! is_null($next_serp_url_research)) {
CrawlUrlResearchJob::dispatch($next_serp_url_research->id)->onQueue('default')->onConnection('default');
}
}
}
private static function getMainImageFromHtml($html)
{
$r_configuration = new ReadabilityConfiguration();
$r_configuration->setCharThreshold(20);
$readability = new Readability($r_configuration);
try {
$readability->parse($html);
return $readability->getImage();
//dd($readability);
} catch (ReadabilityParseException $e) {
}
return null;
}
private static function getMarkdownFromHtml($html)
{
$converter = new HtmlConverter([
'strip_tags' => true,
'strip_placeholder_links' => true,
]);
$html = self::cleanHtml($html);
$markdown = $converter->convert($html);
//dd($markdown);
$markdown = self::reverseLTGT($markdown);
$markdown = self::normalizeNewLines($markdown);
$markdown = self::removeDuplicateLines($markdown);
return html_entity_decode(markdown_to_plaintext($markdown));
}
private static function reverseLTGT($input)
{
$output = str_replace('&lt;', '<', $input);
$output = str_replace('&gt;', '>', $output);
return $output;
}
private static function removeDuplicateLines($string)
{
$lines = explode("\n", $string);
$uniqueLines = array_unique($lines);
return implode("\n", $uniqueLines);
}
private static function normalizeNewLines($content)
{
// Split the content by lines
$lines = explode("\n", $content);
$processedLines = [];
for ($i = 0; $i < count($lines); $i++) {
$line = trim($lines[$i]);
// If the line is an image markdown
if (preg_match("/^!\[.*\]\(.*\)$/", $line)) {
// And if the next line is not empty and not another markdown structure
if (isset($lines[$i + 1]) && ! empty(trim($lines[$i + 1])) && ! preg_match('/^[-=#*&_]+$/', trim($lines[$i + 1]))) {
$line .= ' '.trim($lines[$i + 1]);
$i++; // Skip the next line as we're merging it
}
}
// Add line to processedLines if it's not empty
if (! empty($line)) {
$processedLines[] = $line;
}
}
// Collapse excessive newlines
$result = preg_replace("/\n{3,}/", "\n\n", implode("\n", $processedLines));
// Detect and replace the pattern
$result = preg_replace('/^(!\[.*?\]\(.*?\))\s*\n\s*([^\n!]+)/m', '$1 $2', $result);
// Replace multiple spaces with a dash separator
$result = preg_replace('/ {2,}/', ' - ', $result);
return $result;
}
private static function cleanHtml($htmlContent)
{
$crawler = new Crawler($htmlContent);
// Define tags to remove completely
$tagsToRemove = ['script', 'style', 'svg', 'picture', 'form', 'footer', 'nav', 'aside'];
foreach ($tagsToRemove as $tag) {
$crawler->filter($tag)->each(function ($node) {
foreach ($node as $child) {
$child->parentNode->removeChild($child);
}
});
}
// Replace <span> tags with their inner content
$crawler->filter('span')->each(function ($node) {
$replacement = new \DOMText($node->text());
foreach ($node as $child) {
$child->parentNode->replaceChild($replacement, $child);
}
});
return $crawler->outerHtml();
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace App\Jobs\Tasks;
use App\Helpers\FirstParty\OpenAI\OpenAI;
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
use App\Jobs\SchedulePublishPost;
use App\Models\Entity;
use App\Models\PostEntity;
use App\Models\Category;
use App\Models\Post;
use App\Models\PostCategory;
use App\Models\SerpUrlResearch;
use App\Models\ServiceCostUsage;
use Exception;
use Illuminate\Support\Facades\Http;
use Image;
class FillPostMetadataTask
{
public static function handle(int $post_id)
{
$post = Post::find($post_id);
if (is_null($post)) {
return;
}
if (! is_null($post->metadata)) {
$post_meta_response = $post->metadata;
} else {
$post_meta_response = OpenAI::getArticleMeta($post->body, 1536, 30);
if ((isset($post_meta_response->output)) && (! is_null($post_meta_response->output))) {
$service_cost_usage = new ServiceCostUsage;
$service_cost_usage->cost = $post_meta_response->cost;
$service_cost_usage->name = 'openai-getArticleMeta';
$service_cost_usage->reference_1 = 'post';
$service_cost_usage->reference_2 = strval($post->id);
$service_cost_usage->output = $post_meta_response;
$service_cost_usage->save();
}
}
//dump($post_meta_response);
if ((isset($post_meta_response->output)) && (! is_null($post_meta_response->output))) {
$post->metadata = $post_meta_response;
if (isset($post_meta_response->output->keywords)) {
if (count($post_meta_response->output->keywords) > 0) {
$post->keywords = $post_meta_response->output->keywords;
$post->main_keyword = $post_meta_response->output->keywords[0];
}
}
if (isset($post_meta_response->output->title)) {
if ((is_empty($post->title)) && (! is_empty($post_meta_response->output->title))) {
$post->title = $post_meta_response->output->title;
}
}
if (isset($post_meta_response->output->summary)) {
if (! is_empty($post_meta_response->output->summary)) {
$post->bites = $post_meta_response->output->summary;
}
}
}
if (is_empty($post->slug)) {
$post->slug = str_slug($post->title);
}
if (is_empty($post->featured_image)) {
$post = self::setPostImage($post);
}
if (isset($post_meta_response->output->society_impact))
{
if (!is_empty($post_meta_response->output->society_impact))
{
$post->society_impact = $post_meta_response->output->society_impact;
}
}
if (isset($post_meta_response->output->society_impact_level))
{
if (!is_empty($post_meta_response->output->society_impact_level))
{
$post->society_impact = $post_meta_response->output->society_impact_level;
}
}
if ($post->save()) {
// Set Category
$category_name = 'Updates';
if ((isset($post_meta_response->output->category)) && (!is_empty($post_meta_response->output->category)))
{
$category_name = $post_meta_response?->output?->category;
}
$category = Category::where('name', $category_name)->first();
if (is_null($category))
{
$category = Category::where('name', 'Updates')->first();
}
// Set Post Category
$post_category = PostCategory::where('post_id', $post->id)->first();
if (is_null($post_category))
{
$post_category = new PostCategory;
$post_category->post_id = $post->id;
}
$post_category->category_id = $category->id;
$post_category->save();
// Set Post Entities
if (isset($post_meta_response->output->entities))
{
$entity_names = [];
if (is_array($post_meta_response->output->entities))
{
$entity_names = $post_meta_response->output->entities;
}
if (count($entity_names) > 0)
{
$previous_post_entities = PostEntity::where('post_id', $post->id)->delete();
foreach ($entity_names as $entity_name)
{
$entity_name = trim($entity_name);
$entity = Entity::where('name', $entity_name)->first();
if (is_null($entity))
{
$entity = new Entity;
$entity->name = $entity_name;
$entity->slug = str_slug($entity_name);
$entity->save();
}
$post_entity = PostEntity::where('post_id', $post->id)
->where('entity_id', $entity->id)
->first();
if (is_null($post_entity))
{
$post_entity = new PostEntity;
$post_entity->post_id = $post->id;
$post_entity->entity_id = $entity->id;
$post_entity->save();
}
}
}
}
// Set Schedule Publish
SchedulePublishPost::dispatch($post->id, 'future')->onQueue('default')->onConnection('default');
}
}
private static function setPostImage($post)
{
$serp_url_researches = SerpUrlResearch::where('serp_url_id', $post->serp_url_id)->get();
$main_image_url = null;
foreach ($serp_url_researches as $serp_url_research) {
if (! is_empty($serp_url_research->main_image)) {
if (is_valid_url($serp_url_research->main_image)) {
if (is_empty($serp_url_research->main_image)) {
continue;
}
$main_image_url = $serp_url_research->main_image;
$image_response = Http::timeout(300)->withHeaders([
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
])->get($main_image_url);
$image_content = $image_response->body();
// Get the size of the image content in KB
$imageSizeInKb = strlen($image_response->body()) / 1024;
// Skip this iteration if the image exceeds the maximum size
if ($imageSizeInKb > 1024) {
continue;
}
$canvas_width = 1080;
$canvas_height = 608;
$thumb_width = 540;
$thumb_height = 78;
// Create an image from the fetched content
$image = Image::make($image_content);
// Check if the image is wider than it is tall (landscape orientation)
if ($image->width() > $image->height()) {
// Resize the image to fill the canvas width while maintaining aspect ratio
$image->resize($canvas_width, null, function ($constraint) {
$constraint->aspectRatio();
});
} else {
// Resize the image to fill the canvas height while maintaining aspect ratio
$image->resize(null, $canvas_height, function ($constraint) {
$constraint->aspectRatio();
});
}
// Fit the image to the canvas size, without gaps
$image->fit($canvas_width, $canvas_height, function ($constraint) {
$constraint->upsize();
});
// Create a thumbnail by cloning the original image and resizing it
$thumb = clone $image;
$thumb->resize($thumb_width, $thumb_height, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
// Create a image filename
$epoch_now_timestamp = epoch_now_timestamp();
$filename = $post->slug.'-'.$epoch_now_timestamp.'.jpg';
$thumb_filename = $post->slug.'-'.$epoch_now_timestamp.'_thumb.jpg';
OSSUploader::uploadFile('r2', 'post_images_2/', $filename, (string) $image->stream('jpeg', 75));
OSSUploader::uploadFile('r2', 'post_images_2/', $thumb_filename, (string) $thumb->stream('jpeg', 50));
$post->featured_image = 'post_images_2/'.$filename;
$image->destroy();
try {
break;
} catch (Exception $e) {
continue;
}
}
}
}
return $post;
}
}

View File

@@ -94,7 +94,7 @@ public static function handle($post)
}
}
//return $news_serp_result;
//return $news_serp_result;
} else {
throw new Exception('Uploading failed', 1);
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Jobs\Tasks;
use App\Helpers\FirstParty\OpenAI\OpenAI;
use App\Jobs\BrowseDFSForResearchJob;
use App\Models\SerpUrl;
use App\Models\ServiceCostUsage;
class IdentifyCrawlSourcesTask
{
public static function handle(int $serp_url_id)
{
$serp_url = SerpUrl::find($serp_url_id);
if (! is_null($serp_url)) {
$attempt = 0;
$maxAttempts = 3;
$suggestion_response = null;
while ($attempt < $maxAttempts && ($suggestion_response === null || $suggestion_response->output === null)) {
$suggestion_response = OpenAI::titleSuggestions($serp_url->title);
//dump($suggestion_response);
$service_cost_usage = new ServiceCostUsage;
$service_cost_usage->cost = $suggestion_response->cost;
$service_cost_usage->name = 'openai-titleSuggestions';
$service_cost_usage->reference_1 = 'serp_url';
$service_cost_usage->reference_2 = strval($serp_url->id);
$service_cost_usage->output = $suggestion_response;
$service_cost_usage->save();
$attempt++;
// If the output is not null, break out of the loop
if ($suggestion_response !== null && $suggestion_response->output !== null) {
break;
}
// Optional: sleep for a bit before retrying
sleep(1); // sleep for 1 second
}
if (! is_null($suggestion_response->output)) {
$serp_url->suggestion_data = $suggestion_response->output;
if ($serp_url->save()) {
BrowseDFSForResearchJob::dispatch($serp_url_id)->onQueue('default')->onConnection('default');
}
}
}
}
}

View File

@@ -2,15 +2,19 @@
namespace App\Jobs\Tasks;
use App\Helpers\FirstParty\OpenAI\OpenAI;
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
use App\Jobs\IdentifyCrawlSourcesJob;
use App\Models\Category;
use App\Models\NewsSerpResult;
use App\Models\SerpUrl;
use App\Models\ServiceCostUsage;
use Carbon\Carbon;
use Exception;
class ParseNewsSerpDomainsTask
class ParseDFSNewsTask
{
public static function handle(NewsSerpResult $news_serp_result, $serp_counts = 1)
public static function handle(NewsSerpResult $news_serp_result, $serp_counts = 100)
{
//dd($news_serp_result->category->serp_at);
@@ -35,16 +39,47 @@ public static function handle(NewsSerpResult $news_serp_result, $serp_counts = 1
foreach ($serp_results as $serp_item) {
if ($serp_item->type != 'news_search') {
continue;
}
//dump($serp_item);
if (is_empty($serp_item->url)) {
continue;
}
// if (!str_contains($serp_item->time_published, "hours"))
// {
// continue;
// }
$blacklist_keywords = config('platform.global.blacklist_keywords_serp');
$blacklist_domains = config('platform.global.blacklist_domains_serp');
$skipItem = false;
foreach ($blacklist_domains as $domain) {
if (str_contains($serp_item->domain, $domain)) {
$skipItem = true;
break;
}
}
if (! $skipItem) {
$title = strtolower($serp_item->title);
$snippet = strtolower($serp_item->snippet);
// Check if any unwanted word is in the title or snippet
foreach ($blacklist_keywords as $word) {
if (strpos($title, $word) !== false || strpos($snippet, $word) !== false) {
$skipItem = true;
break; // Break the inner loop as we found an unwanted word
}
}
}
// Skip this iteration if an unwanted word was found
if ($skipItem) {
continue;
}
$serp_url = SerpUrl::where('url', $serp_item->url)->first();
@@ -69,14 +104,14 @@ public static function handle(NewsSerpResult $news_serp_result, $serp_counts = 1
//dd($valid_serps);
$serp_titles = [];
foreach ($valid_serps as $serp_item) {
//dd($serp_item);
$serp_url = SerpUrl::where('url', self::normalizeUrl($serp_item->url))->first();
if (is_null($serp_url)) {
$serp_url = new SerpUrl;
$serp_url->category_id = $news_serp_result->category_id;
$serp_url->category_name = $news_serp_result->category_name;
$serp_url->news_serp_result_id = $news_serp_result->id;
}
@@ -85,20 +120,47 @@ public static function handle(NewsSerpResult $news_serp_result, $serp_counts = 1
$serp_url->country_iso = $news_serp_result->serp_country_iso;
if (! is_empty($serp_item->title)) {
$serp_url->title = $serp_item->title;
$serp_url->title = remove_newline($serp_item->title);
}
if (! is_empty($serp_item->snippet)) {
$serp_url->description = $serp_item->snippet;
$serp_url->description = remove_newline($serp_item->snippet);
}
if ($serp_url->isDirty()) {
$serp_url->serp_at = $news_serp_result->category->serp_at;
$serp_url->serp_at = now();
}
if ((isset($serp_item->timestamp)) && (! is_empty($serp_item->timestamp))) {
$serp_url->url_posted_at = Carbon::parse($serp_item->timestamp);
} else {
$serp_url->url_posted_at = now();
}
if ($serp_url->save()) {
$success = true;
}
$serp_titles[$serp_url->id] = $serp_url->title;
}
$ids_response = OpenAI::topTitlePicksById(json_encode($serp_titles));
if (isset($ids_response->output->ids)) {
$service_cost_usage = new ServiceCostUsage;
$service_cost_usage->cost = $ids_response->cost;
$service_cost_usage->name = 'openai-topTitlePicksById';
$service_cost_usage->reference_1 = 'news_serp_result';
$service_cost_usage->reference_2 = strval($news_serp_result->id);
$service_cost_usage->output = $ids_response;
$service_cost_usage->save();
$selected_serp_urls = SerpUrl::whereIn('id', $ids_response->output->ids)->update(['picked' => true]);
foreach ($ids_response->output->ids as $id) {
IdentifyCrawlSourcesJob::dispatch($id)->onQueue('default')->onConnection('default');
}
}
}

View File

@@ -9,12 +9,19 @@
class PublishIndexPostTask
{
public static function handle(Post $post)
public static function handle(int $post_id)
{
$post->published_at = now();
$post = Post::find($post_id);
if (is_null($post))
{
return ;
}
$post->status = 'publish';
if ($post->save()) {
if (app()->environment() == 'production') {
if ((app()->environment() == 'production') && (config('platform.global.indexing'))) {
$post_url = route('front.post', ['slug' => $post->slug, 'category_slug' => $post->category->slug]);
try {

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Jobs\Tasks;
use App\Models\Post;
use App\Notifications\PostIncomplete;
use Notification;
class SchedulePublishTask
{
public static function handle($post_id, $post_status = 'publish')
{
sleep(2);
$post = Post::find($post_id);
if (is_null($post)) {
return;
}
if (! in_array($post->status, ['future', 'draft', 'publish'])) {
return;
}
if ((is_empty($post->title)) || (is_empty($post->slug)) || (is_empty($post->main_keyword)) || (is_empty($post->keywords)) || (is_empty($post->bites)) || (is_empty($post->featured_image)) || (is_empty($post->body)) || (is_empty($post->metadata))) {
Notification::route(get_notification_channel(), get_notification_user_id())->notify(new PostIncomplete($post));
return;
}
/*
TODO:
- to determine a published_at time, first check if there are any post with existing published_at date.
- if there are no other posts except for the current post, then the current post published_at is now().
- if there are other posts but all of them published_at is null, then the current post published_at is now().
- if there are other posts and there are non null published_at,
-- first find the latest published post (latest published_at).
-- if the latest published_at datetime is before now, then published_at is null.
-- if the latest published_at datetime is after now, then current post published_at should be 1 hour after the latest published_at
-- the idea is published_posts should be spreaded accross by an hour if found.
*/
// Check if there are any other posts with a set published_at date
$latest_published_post = Post::where('id', '!=', $post_id)->whereNotNull('published_at')->orderBy('published_at', 'DESC')->first();
//dd($latest_published_post);
if (is_null($latest_published_post)) {
$post->published_at = now();
} else {
if ($latest_published_post->published_at->lt(now())) {
$new_time = now();
} else {
$new_time = clone $latest_published_post->published_at;
}
$new_time->addMinutes(rand(40, 60));
$post->published_at = $new_time;
}
$post->published_at->subDays(1);
$post->status = $post_status; // Assuming you want to update the status to future
$post->save();
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Jobs\Tasks;
use App\Helpers\FirstParty\OpenAI\OpenAI;
use App\Jobs\FillPostMetadataJob;
use App\Models\Post;
use App\Models\SerpUrl;
use App\Models\SerpUrlResearch;
use App\Models\ServiceCostUsage;
use Exception;
use Mis3085\Tiktoken\Facades\Tiktoken;
class WriteWithAITask
{
public static function handle(int $serp_url_id)
{
$serp_url = SerpUrl::find($serp_url_id);
if (is_null($serp_url)) {
return;
}
$serp_url_researches = SerpUrlResearch::where('serp_url_id', $serp_url->id)->where('content', '!=', 'EMPTY CONTENT')->whereNotNull('content')->get();
$user_prompt = '';
$total_tokens = 0;
foreach ($serp_url_researches as $serp_url_research) {
$sentences = self::markdownToSentences($serp_url_research->content);
//dump($sentences);
foreach ($sentences as $key => $sentence) {
if ($key == 0) {
$user_prompt .= "ARTICLE:\n";
}
$current_tokens = Tiktoken::count($sentence);
if ($current_tokens + $total_tokens > 4096) {
break 2;
} else {
$user_prompt .= $sentence."\n";
$total_tokens += $current_tokens;
}
}
$user_prompt .= "\n\n";
}
//dd($user_prompt);
$ai_writeup_response = OpenAI::writeArticle($user_prompt, 1536, 30);
//dd($ai_writeup_response);
if ((isset($ai_writeup_response->output)) && (! is_empty($ai_writeup_response->output))) {
$output = self::extractRemoveFirstHeading($ai_writeup_response->output);
$service_cost_usage = new ServiceCostUsage;
$service_cost_usage->cost = $ai_writeup_response->cost;
$service_cost_usage->name = 'openai-writeArticle';
$service_cost_usage->reference_1 = 'serp_url';
$service_cost_usage->reference_2 = strval($serp_url->id);
$service_cost_usage->output = $ai_writeup_response;
$service_cost_usage->save();
$post = Post::where('serp_url_id', $serp_url->id)->first();
if (is_null($post)) {
$post = new Post;
$post->serp_url_id = $serp_url->id;
}
if (! is_empty($output->title)) {
$post->title = $output->title;
} else {
if (! is_null($serp_url->suggestion_data)) {
if (isset($serp_url->suggestion_data->article_headings)) {
if (count($serp_url->suggestion_data->article_headings) > 0) {
$post->title = $serp_url->suggestion_data?->article_headings[0];
}
}
}
}
if (is_empty($post->title)) {
$post->title = $serp_url->title;
}
$post->slug = str_slug($post->title);
$post->body = $output->content;
$post->bites = null;
$post->metadata = null;
if ($post->save()) {
FillPostMetadataJob::dispatch($post->id)->onQueue('default')->onConnection('default');
}
} else {
throw new Exception('OpenAI failed to write');
}
}
private static function markdownToSentences($markdownContent)
{
// Split the content on punctuation followed by a space or end of string
$pattern = '/(?<=[.!?])\s+|\z/';
// Split the content into sentences
$sentences = preg_split($pattern, $markdownContent, -1, PREG_SPLIT_NO_EMPTY);
// Return the array of sentences
return $sentences;
}
private static function extractRemoveFirstHeading($markdownContent)
{
// Pattern to match the first markdown heading of any level
$pattern = '/^(#+)\s*(.+)$/m';
// Try to find the first heading
if (preg_match($pattern, $markdownContent, $matches)) {
$title = $matches[2]; // The first heading becomes the title
// Remove the first heading from the content
$updatedContent = preg_replace($pattern, '', $markdownContent, 1);
return (object) ['title' => $title, 'content' => trim($updatedContent)];
}
// Return original content if no heading found
return (object) ['title' => '', 'content' => $markdownContent];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Jobs;
use App\Jobs\Tasks\WriteWithAITask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class WriteWithAIJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $serp_url_id;
public $timeout = 240;
/**
* Create a new job instance.
*/
public function __construct($serp_url_id)
{
$this->serp_url_id = $serp_url_id;
}
/**
* Execute the job.
*/
public function handle(): void
{
WriteWithAITask::handle($this->serp_url_id);
}
}

39
app/Models/Entity.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
/**
* Class Entity
*
* @property int $id
* @property string $name
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*
* @property Collection|PostEntity[] $post_entities
*
* @package App\Models
*/
class Entity extends Model
{
protected $table = 'entities';
protected $fillable = [
'name',
'slug',
'description'
];
public function post_entities()
{
return $this->hasMany(PostEntity::class);
}
}

View File

@@ -7,6 +7,7 @@
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
@@ -17,74 +18,93 @@
* Class Post
*
* @property int $id
* @property int|null $serp_url_id
* @property string|null $title
* @property string|null $slug
* @property string|null $type
* @property string|null $excerpt
* @property string|null $main_keyword
* @property string|null $keywords
* @property string|null $bites
* @property int|null $author_id
* @property bool $featured
* @property string|null $featured_image
* @property string|null $body
* @property int $views_count
* @property string $status
* @property Carbon|null $published_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property SerpUrl|null $serp_url
* @property Author|null $author
* @property Collection|PostCategory[] $post_categories
* @property Carbon $published_at
*/
class Post extends Model implements Feedable
{
protected $table = 'posts';
protected $casts = [
'serp_url_id' => 'int',
'author_id' => 'int',
'featured' => 'bool',
'views_count' => 'int',
'keywords' => 'array',
'published_at' => 'datetime',
'keywords' => 'array',
'metadata' => 'object',
];
protected $fillable = [
'serp_url_id',
'title',
'short_title',
'slug',
'type',
'excerpt',
'main_keyword',
'keywords',
'bites',
'author_id',
'featured',
'featured_image',
'body',
'views_count',
'status',
'main_keyword',
'keywords',
'published_at',
'metadata',
'society_impact',
'society_impact_level',
];
public function getFeaturedImageLqipCdnAttribute()
protected function featuredImage(): Attribute
{
if (! is_empty($this->featured_image)) {
// Get the extension of the original featured image
$extension = pathinfo($this->featured_image, PATHINFO_EXTENSION);
return Attribute::make(
get: function ($value = null) {
if (! is_empty($value)) {
return Storage::disk('r2')->url($value);
}
// Append "_lqip" before the extension to create the LQIP image URL
$lqipFeaturedImage = str_replace(".{$extension}", '_lqip.webp', $this->featured_image);
return null;
}
);
}
return 'https://'.Storage::disk('r2')->url($lqipFeaturedImage).'?a=bc';
protected function getFeaturedThumbImageAttribute()
{
$value = $this->featured_image;
//dd($value);
if (! is_empty($value)) {
// Extract the file extension
$extension = pathinfo($value, PATHINFO_EXTENSION);
// Construct the thumbnail filename by appending '_thumb' before the extension
$thumbnail = str_replace(".{$extension}", "_thumb.{$extension}", $value);
return $thumbnail;
// Return the full URL to the thumbnail image
//return Storage::disk('r2')->url($thumbnail);
}
return null;
}
public function getFeaturedImageCdnAttribute()
public function serp_url()
{
if (! is_empty($this->featured_image)) {
return 'https://'.Storage::disk('r2')->url($this->featured_image);
}
return null;
return $this->belongsTo(SerpUrl::class);
}
public function author()
@@ -92,6 +112,16 @@ public function author()
return $this->belongsTo(Author::class);
}
public function post_categories()
{
return $this->hasMany(PostCategory::class);
}
public function post_entities()
{
return $this->hasMany(PostEntity::class);
}
public function category()
{
return $this->hasOneThrough(
@@ -104,15 +134,27 @@ public function category()
);
}
public function entities()
{
return $this->hasManyThrough(
Entity::class, // The target model
PostEntity::class, // The through model
'post_id', // The foreign key on the through model
'id', // The local key on the parent model (Post)
'id', // The local key on the through model (PostEntity)
'entity_id' // The foreign key on the target model (Entity)
);
}
public function toFeedItem(): FeedItem
{
return FeedItem::create([
'id' => $this->id,
'title' => $this->title,
'summary' => $this->excerpt,
'summary' => $this->bites,
'updated' => $this->updated_at,
'link' => route('front.post', ['slug' => $this->slug, 'category_slug' => $this->category->slug]),
'authorName' => optional($this->author)->name,
'authorName' => 'FutureWalker',
]);
}

49
app/Models/PostEntity.php Normal file
View File

@@ -0,0 +1,49 @@
<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
/**
* Class PostEntity
*
* @property int $id
* @property int $post_id
* @property int $entity_id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*
* @property Post $post
* @property Entity $entity
*
* @package App\Models
*/
class PostEntity extends Model
{
protected $table = 'post_entities';
protected $casts = [
'post_id' => 'int',
'entity_id' => 'int'
];
protected $fillable = [
'post_id',
'entity_id'
];
public function post()
{
return $this->belongsTo(Post::class);
}
public function entity()
{
return $this->belongsTo(Entity::class);
}
}

View File

@@ -37,6 +37,12 @@ class SerpUrl extends Model
'category_id' => 'int',
'process_status' => 'int',
'serp_at' => 'datetime',
'picked' => 'boolean',
'processed' => 'boolean',
'crawled' => 'boolean',
'written' => 'boolean',
'url_posted_at' => 'datetime',
'suggestion_data' => 'object',
];
protected $fillable = [
@@ -51,6 +57,12 @@ class SerpUrl extends Model
'process_status',
'serp_at',
'status',
'picked',
'processed',
'crawled',
'written',
'url_posted_at',
'suggestion_data',
];
public function news_serp_result()

View File

@@ -0,0 +1,44 @@
<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
/**
* Class SerpUrlResearch
*
* @property int $id
* @property int $serp_url_id
* @property string $url
* @property string $query
* @property string|null $content
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property SerpUrl $serp_url
*/
class SerpUrlResearch extends Model
{
protected $table = 'serp_url_researches';
protected $casts = [
'serp_url_id' => 'int',
];
protected $fillable = [
'serp_url_id',
'url',
'query',
'content',
'main_image',
];
public function serp_url()
{
return $this->belongsTo(SerpUrl::class);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
/**
* Class ServiceCostUsage
*
* @property int $id
* @property float $cost
* @property string $name
* @property string|null $reference_1
* @property string|null $reference_2
* @property string $output
* @property string|null $input_1
* @property string|null $input_2
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*/
class ServiceCostUsage extends Model
{
protected $table = 'service_cost_usages';
protected $casts = [
'cost' => 'float',
'output' => 'object',
];
protected $fillable = [
'cost',
'name',
'reference_1',
'reference_2',
'output',
'input_1',
'input_2',
];
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Notifications;
use App\Models\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use NotificationChannels\Telegram\TelegramMessage;
class PostIncomplete extends Notification
{
use Queueable;
protected $post;
/**
* Create a new notification instance.
*/
public function __construct(Post $post)
{
$this->post = $post;
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['telegram'];
}
public function toTelegram($notifiable)
{
return TelegramMessage::create()
->content("*Incomplete Post*:\nPost ID: ".$this->post->id."\nPost Name: ".$this->post->title);
}
}

View File

@@ -12,7 +12,7 @@
"artesaos/seotools": "^1.2",
"dipeshsukhia/laravel-html-minify": "^3.3",
"famdirksen/laravel-google-indexing": "^0.5.0",
"fivefilters/readability.php": "^1.0",
"fivefilters/readability.php": "^3.1.6",
"graham-campbell/markdown": "^15.0",
"guzzlehttp/guzzle": "^7.2",
"inspector-apm/inspector-laravel": "^4.7",
@@ -20,10 +20,13 @@
"jovix/dataforseo-clientv3": "^1.1",
"kalnoy/nestedset": "^6.0",
"laravel-freelancer-nl/laravel-index-now": "^1.2",
"laravel-notification-channels/telegram": "^4.0",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8",
"league/flysystem-aws-s3-v3": "^3.0",
"league/html-to-markdown": "^5.1",
"mis3085/tiktoken-for-laravel": "^0.1.2",
"predis/predis": "^2.2",
"silviolleite/laravelpwa": "^2.0",
"spatie/laravel-feed": "^4.3",

1592
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,8 +18,8 @@
*/
'url' => '/posts-feed',
'title' => 'Latest News from EchoScoop',
'description' => 'Bite-sized scoop for world news.',
'title' => 'Latest News from FutureWalker',
'description' => 'AI & technology news you should not miss.',
'language' => 'en-US',
/*

View File

@@ -2,7 +2,7 @@
return [
'google' => [
'auth_config' => storage_path('echoscoop-90d335332507.json'),
'auth_config' => storage_path('futurewalker-8a2531e98458.json'),
'scopes' => [
\Google_Service_Indexing::INDEXING,

View File

@@ -1,10 +1,10 @@
<?php
return [
'name' => 'EchoScoop',
'name' => 'FutureWalker',
'manifest' => [
'name' => env('APP_NAME', 'EchoScoop'),
'short_name' => 'EchoScoop',
'name' => env('APP_NAME', 'FutureWalker'),
'short_name' => 'FutureWalker',
'start_url' => '/',
'background_color' => '#ffffff',
'theme_color' => '#000000',

View File

@@ -6,4 +6,42 @@
'api_key' => env('OPENAI_API_KEY'),
],
// GPT 4
'openai-gpt-4-turbo' => [
'tokens' => 128000,
'model' => 'gpt-4-1106-preview',
'input_cost_per_thousand_tokens' => 0.01,
'output_cost_per_thousand_tokens' => 0.03,
],
'openai-gpt-4' => [
'tokens' => 8192,
'model' => 'gpt-4-0613',
'input_cost_per_thousand_tokens' => 0.03,
'output_cost_per_thousand_tokens' => 0.06,
],
// GPT 3.5
'openai-gpt-3-5-turbo-1106' => [
'tokens' => 16385,
'model' => 'gpt-3.5-turbo-1106',
'input_cost_per_thousand_tokens' => 0.0010,
'output_cost_per_thousand_tokens' => 0.002,
],
'openai-3-5-turbo' => [
'tokens' => 4096,
'model' => 'gpt-3.5-turbo',
'input_cost_per_thousand_tokens' => 0.0015,
'output_cost_per_thousand_tokens' => 0.002,
],
'openai-3-5-turbo-16k' => [
'tokens' => 16385,
'model' => 'gpt-3.5-turbo-16k',
'input_cost_per_thousand_tokens' => 0.003,
'output_cost_per_thousand_tokens' => 0.004,
],
];

View File

@@ -2,6 +2,95 @@
return [
'indexing' => env('ENABLE_INDEXING', false),
'launched_epoch' => '1695513600', // 24-09-2023 00:00:00 GMT +0
'blacklist_domains_serp' => $paywalled_domains = [
],
'blacklist_keywords_serp' => [
'government',
'usa',
'china',
'policy',
'trade',
'deal',
'fake',
'nude',
'risk',
'disclosure',
'politic',
'contract',
'negotiat',
'complete',
'gun',
'safety',
'wrest',
'control',
'opinion',
'cop',
'race',
'porn',
'regulat',
'rule',
'stock',
'WSJ',
'complicated',
'leverag',
'attack',
'defend',
'concern',
'biden',
':',
'surviv',
'tackl',
'compet',
'legal',
'securit',
'alert',
'state of',
'error',
'licens',
'department of',
'threat',
'democra',
'asia',
'japan',
'doubt',
'agenc',
'presiden',
'avoid',
'study',
'expert',
'agreement',
'protection',
'survey',
'law',
'military',
'lose',
'destroy',
'humanity',
'lose',
'concern',
'ignore',
'contradic',
'wishful',
'scammer',
'fear',
'?',
'paranoid',
'copyright',
'capitaliz',
'strike',
'$',
'weapon',
'concern',
'ethic',
'underage',
'guide',
],
];

25
config/platform/proxy.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
return [
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246',
'smartproxy' => [
'rotating_global' => [
'user' => 'sp5bbkzj7e',
'password' => 'yTtk2cc5kg23kIkSSr',
'server' => 'gate.smartproxy.com:7000',
'reproxy' => '157.230.194.206:7000',
'reproxy_enable' => false,
'cost_per_gb' => 7,
],
'unblocker' => [
'user' => 'U0000123412',
'password' => 'P$W1bda906aee53c2022d94e22ff1a1142a1',
'server' => 'unblock.smartproxy.com:60000',
'reproxy' => '157.230.194.206:7000',
'reproxy_enable' => false,
'cost_per_gb' => 20.14,
],
],
];

View File

@@ -12,9 +12,9 @@
* The default configurations to be used by the meta generator.
*/
'defaults' => [
'title' => 'EchoScoop: Bite-sized world news', // set false to total remove
'title' => 'FutureWalker: Daily AI & tech news', // set false to total remove
'titleBefore' => false, // Put defaults.title before page title, like 'It's Over 9000! - Dashboard'
'description' => 'Distilling world news into bite-sized scoops.', // set false to total remove
'description' => 'Stay updated with critical AI & tech news to build your future.', // set false to total remove
'separator' => ' - ',
'keywords' => [],
'canonical' => 'current', // Set to null or 'full' to use Url::full(), set to 'current' to use Url::current(), set false to total remove
@@ -39,12 +39,12 @@
* The default configurations to be used by the opengraph generator.
*/
'defaults' => [
'title' => 'EchoScoop: Bite-sized world news', // set false to total remove
'description' => 'Distilling world news into bite-sized scoops.', // set false to total remove
'title' => 'FutureWalker: Daily AI & tech news', // set false to total remove
'description' => 'Stay updated with critical AI & tech news to build your future.', // set false to total remove
'url' => false, // Set null for using Url::current(), set false to total remove
'type' => false,
'site_name' => false,
'images' => ['https://echoscoop.com/pwa/icon-512x512.png'],
//'images' => ['https://FutureWalker.com/pwa/icon-512x512.png'],
],
],
'twitter' => [
@@ -61,11 +61,11 @@
* The default configurations to be used by the json-ld generator.
*/
'defaults' => [
'title' => 'EchoScoop: Bite-sized world news', // set false to total remove
'description' => 'Distilling world news into bite-sized scoops.', // set false to total remove
'title' => 'FutureWalker: Daily AI & tech news', // set false to total remove
'description' => 'Stay updated with critical AI & tech news to build your future.', // set false to total remove
'url' => false, // Set to null or 'full' to use Url::full(), set to 'current' to use Url::current(), set false to total remove
'type' => 'WebPage',
'images' => ['https://echoscoop.com/pwa/icon-512x512.png'],
//'images' => ['https://FutureWalker.com/pwa/icon-512x512.png'],
],
],
];

View File

@@ -31,4 +31,8 @@
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'telegram-bot-api' => [
'token' => env('TELEGRAM_BOT_TOKEN'),
],
];

15
config/tiktoken.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
// config for Tiktoken
return [
// Cache folder for vocab files
'cache_dir' => storage_path('framework/cache/tiktoken'),
/**
* The default encoder
* cl100k_base: gpt-4, gpt-3.5-turbo, text-embedding-ada-002
* p50k_base: Codex models, text-davinci-002, text-davinci-003
* r50k_base: text-davinci-001
*/
'default_encoder' => 'cl100k_base',
];

View File

@@ -14,13 +14,19 @@ public function up(): void
Schema::create('serp_urls', function (Blueprint $table) {
$table->id();
$table->foreignId('news_serp_result_id');
$table->foreignId('category_id');
$table->string('category_name');
$table->foreignId('category_id')->nullable();
$table->string('category_name')->nullable();
$table->string('source')->default('serp');
$table->string('url');
$table->string('country_iso');
$table->string('title')->nullable();
$table->text('description')->nullable();
$table->boolean('picked')->default(false);
$table->boolean('processed')->default(false);
$table->boolean('crawled')->default(false);
$table->boolean('written')->default(false);
$table->json('suggestion_data')->nullable();
$table->datetime('url_posted_at');
$table->timestamp('serp_at')->nullable();
$table->enum('status', ['initial', 'processing', 'complete', 'failed', 'blocked', 'limited'])->default('initial');
$table->tinyInteger('process_status')->nullable();

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('serp_url_researches', function (Blueprint $table) {
$table->id();
$table->foreignId('serp_url_id');
$table->string('url');
$table->string('query');
$table->text('content')->nullable();
$table->mediumText('main_image')->nullable();
$table->timestamps();
$table->foreign('serp_url_id')->references('id')->on('serp_urls');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('serp_url_researches');
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('service_cost_usages', function (Blueprint $table) {
$table->id();
$table->double('cost', 5, 5);
$table->string('name');
$table->string('reference_1')->nullable();
$table->string('reference_2')->nullable();
$table->jsonb('output');
$table->text('input_1')->nullable();
$table->text('input_2')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('service_cost_usages');
}
};

View File

@@ -13,23 +13,25 @@ public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('serp_url_id')->nullable();
$table->string('title')->nullable();
$table->string('short_title')->nullable();
$table->string('slug')->nullable();
$table->string('type')->nullable();
$table->string('main_keyword')->nullable();
$table->json('keywords')->nullable();
$table->mediumText('excerpt')->nullable();
$table->mediumText('bites')->nullable();
$table->mediumText('society_impact')->nullable();
$table->enum('society_impact_level', ['low','medium','high'])->default('low');
$table->foreignId('author_id')->nullable();
$table->boolean('featured')->default(false);
$table->string('featured_image')->nullable();
$table->mediumText('featured_image')->nullable();
$table->text('body')->nullable();
$table->json('metadata')->nullable();
$table->integer('views_count')->default(0);
$table->enum('status', ['publish', 'future', 'draft', 'private', 'trash'])->default('draft');
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->foreign('author_id')->references('id')->on('authors');
$table->foreign('serp_url_id')->references('id')->on('serp_urls');
});
}

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('entities', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug');
$table->mediumText('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('entities');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('post_entities', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id');
$table->foreignId('entity_id');
$table->timestamps();
$table->foreign('post_id')->references('id')->on('posts');
$table->foreign('entity_id')->references('id')->on('entities');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('post_entities');
}
};

View File

@@ -13,7 +13,7 @@ class AuthorSeeder extends Seeder
public function run(): void
{
Author::create([
'name' => 'EchoScoop Team',
'name' => 'FutureWalker Team',
'bio' => null,
'avatar' => null,
'enabled' => true, // Assuming you want this author to be enabled by default

View File

@@ -13,92 +13,12 @@ class CategorySeeder extends Seeder
public function run(): void
{
$categories = [
['name' => 'Automotive', 'short_name' => 'Automotive'],
[
'name' => 'Business',
'short_name' => 'Business',
'children' => [
['name' => 'Trading', 'short_name' => 'Trading'],
['name' => 'Information Technology', 'short_name' => 'IT'],
['name' => 'Marketing', 'short_name' => 'Marketing'],
['name' => 'Office', 'short_name' => 'Office'],
['name' => 'Telecommunications', 'short_name' => 'Telecom'],
],
],
['name' => 'Food & Drink', 'short_name' => 'Food'],
[
'name' => 'Hobbies & Gifts',
'short_name' => 'Hobbies',
'children' => [
['name' => 'Collectibles', 'short_name' => 'Collectibles'],
['name' => 'Pets', 'short_name' => 'Pets'],
['name' => 'Photography', 'short_name' => 'Photography'],
['name' => 'Hunting & Fishing', 'short_name' => 'Hunting'],
],
],
['name' => 'Law', 'short_name' => 'Law'],
['name' => 'Politics', 'short_name' => 'Politics'],
[
'name' => 'Shopping',
'short_name' => 'Shopping',
'children' => [
['name' => 'Home & Garden', 'short_name' => 'Home'],
['name' => 'Fashion & Clothing', 'short_name' => 'Fashion'],
],
],
['name' => 'Real Estate', 'short_name' => 'Real Estate'],
[
'name' => 'Society',
'short_name' => 'Society',
'children' => [
['name' => 'Family', 'short_name' => 'Family'],
['name' => 'Wedding', 'short_name' => 'Wedding'],
['name' => 'Immigration', 'short_name' => 'Immigration'],
[
'name' => 'Education',
'short_name' => 'Education',
'children' => [
['name' => 'Languages', 'short_name' => 'Languages'],
],
],
],
],
[
'name' => 'Wellness',
'short_name' => 'Wellness',
'children' => [
['name' => 'Health', 'short_name' => 'Health'],
['name' => 'Beauty', 'short_name' => 'Beauty'],
['name' => 'Psychology', 'short_name' => 'Psychology'],
['name' => 'Religion & Spirituality', 'short_name' => 'Religion'],
],
],
[
'name' => 'Tips & Tricks',
'short_name' => 'Tips',
'children' => [
['name' => 'How to', 'short_name' => 'How to'],
],
],
[
'name' => 'Travel',
'short_name' => 'Travel',
'children' => [
['name' => 'Holiday', 'short_name' => 'Holiday'],
['name' => 'World Festivals', 'short_name' => 'Festivals'],
['name' => 'Outdoors', 'short_name' => 'Outdoors'],
],
],
[
'name' => 'Technology',
'short_name' => 'Tech',
'children' => [
['name' => 'Computer', 'short_name' => 'Computer'],
['name' => 'Phones', 'short_name' => 'Phones'],
['name' => 'Gadgets', 'short_name' => 'Gadgets'],
['name' => 'Social Networks', 'short_name' => 'Social Networks'],
],
],
['name' => 'Updates', 'short_name' => 'Updates'],
['name' => 'Opinions', 'short_name' => 'Opinions'],
['name' => 'Features', 'short_name' => 'Features'],
['name' => 'New Launches', 'short_name' => 'New Launches'],
['name' => 'How Tos', 'short_name' => 'How Tos'],
['name' => 'Reviews', 'short_name' => 'Reviews'],
];
foreach ($categories as $category) {

8
package-lock.json generated
View File

@@ -1,5 +1,5 @@
{
"name": "echoscoop",
"name": "futurewalker",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -1827,9 +1827,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz",
"integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==",
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",

View File

@@ -3,7 +3,7 @@
# eval 'php artisan optimize:clear';
# eval 'php artisan responsecache:clear';
# eval 'php artisan opcache:clear';
eval 'APP_URL=https://echoscoop.com php artisan ziggy:generate';
eval 'APP_URL=https://FutureWalker.com php artisan ziggy:generate';
eval 'blade-formatter --write resources/**/*.blade.php';
eval './vendor/bin/pint';
eval 'npm run build';

BIN
public/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,7 +0,0 @@
<svg width="523" height="512" viewBox="0 0 523 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.8">
<path d="M246.788 90.3222L203.024 133.608C200.874 135.734 201.074 139.257 203.306 141.296C221.183 157.624 250.077 198.024 250.077 256.316C250.077 314.608 221.183 355.007 203.306 371.336C201.074 373.374 200.874 376.897 203.024 379.023L246.788 422.31C248.644 424.146 251.584 424.271 253.482 422.477C278.031 399.259 321.19 338.526 321.19 256.316C321.19 174.105 278.031 113.373 253.482 90.1549C251.584 88.3606 248.644 88.4858 246.788 90.3222Z" fill="#0F6DFF"/>
<path d="M291.704 45.8956L335.64 2.43971C337.512 0.587612 340.504 0.500375 342.422 2.3063C378.923 36.6909 445.637 130.282 445.637 256.316C445.637 382.35 378.923 475.941 342.422 510.325C340.504 512.131 337.512 512.044 335.64 510.192L291.704 466.736C289.63 464.684 289.721 461.327 291.847 459.329C321.739 431.228 374.524 357.159 374.524 256.316C374.524 155.473 321.739 81.4042 291.847 53.3029C289.721 51.3045 289.63 47.9473 291.704 45.8956Z" fill="#0F6DFF"/>
<path d="M98.7111 271.697V277.596C98.7111 288.164 100.642 295.373 104.505 299.223C108.368 302.992 115.642 304.876 126.326 304.876H146.544C154.927 304.876 161.132 304.098 165.159 302.541C169.186 300.985 171.857 298.24 173.172 294.308C174.076 291.195 175.309 288.819 176.871 287.181C178.515 285.542 180.98 284.723 184.268 284.723C187.719 284.723 190.349 285.624 192.158 287.427C193.966 289.229 194.541 291.85 193.883 295.291C192.075 305.45 187.226 313.027 179.336 318.025C171.446 323.022 160.516 325.521 146.544 325.521H126.326C109.971 325.521 97.807 321.588 89.8349 313.724C81.945 305.859 78 293.817 78 277.596V249.087C78 232.456 81.945 220.25 89.8349 212.467C97.807 204.603 109.971 200.752 126.326 200.916H146.544C162.981 200.916 175.145 204.848 183.035 212.713C190.925 220.496 194.87 232.62 194.87 249.087V261.375C194.87 268.257 191.418 271.697 184.514 271.697H98.7111ZM126.326 221.561C115.642 221.397 108.368 223.24 104.505 227.091C100.642 230.941 98.7111 238.273 98.7111 249.087V251.053H174.159V249.087C174.159 238.355 172.227 231.105 168.364 227.336C164.584 223.486 157.31 221.561 146.544 221.561H126.326Z" fill="#0F6DFF"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,12 +0,0 @@
<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.8" clip-path="url(#clip0_1_2)">
<path d="M378.586 138.414L306.07 210.139C303.921 212.265 304.115 215.778 306.398 217.759C334.06 241.752 381.876 305.878 381.876 399.615C381.876 493.352 334.06 557.478 306.398 581.471C304.115 583.452 303.921 586.965 306.07 589.091L378.586 660.816C380.443 662.652 383.4 662.76 385.323 660.993C423.312 626.088 492.991 530.189 492.991 399.615C492.991 269.041 423.312 173.142 385.323 138.237C383.4 136.47 380.443 136.578 378.586 138.414Z" fill="#0F6DFF"/>
<path d="M444.755 72.968L517.442 1.07372C519.315 -0.778377 522.254 -0.91147 524.19 0.874038C580.754 53.0378 687.444 200.385 687.444 399.615C687.444 598.845 580.754 746.192 524.19 798.356C522.254 800.141 519.315 800.008 517.442 798.156L444.755 726.262C442.68 724.21 442.786 720.839 444.94 718.871C491.203 676.593 576.328 559.693 576.328 399.615C576.328 239.537 491.203 122.636 444.94 80.3589C442.786 78.3907 442.68 75.0197 444.755 72.968Z" fill="#0F6DFF"/>
<path d="M145.362 423.649V432.866C145.362 449.378 148.38 460.643 154.415 466.659C160.451 472.548 171.816 475.492 188.511 475.492H220.102C233.201 475.492 242.896 474.276 249.189 471.844C255.481 469.412 259.655 465.123 261.71 458.979C263.122 454.115 265.049 450.403 267.489 447.842C270.057 445.282 273.91 444.002 279.046 444.002C284.44 444.002 288.55 445.41 291.375 448.226C294.2 451.043 295.099 455.139 294.072 460.515C291.246 476.388 283.67 488.229 271.341 496.037C259.013 503.845 241.933 507.75 220.102 507.75H188.511C162.955 507.75 143.949 501.605 131.492 489.317C119.164 477.028 113 458.211 113 432.866V388.319C113 362.334 119.164 343.261 131.492 331.1C143.949 318.812 162.955 312.795 188.511 313.051H220.102C245.786 313.051 264.792 319.196 277.12 331.484C289.448 343.645 295.613 362.59 295.613 388.319V407.52C295.613 418.273 290.219 423.649 279.432 423.649H145.362ZM188.511 345.309C171.816 345.053 160.451 347.933 154.415 353.949C148.38 359.966 145.362 371.422 145.362 388.319V391.391H263.251V388.319C263.251 371.55 260.233 360.222 254.197 354.333C248.29 348.317 236.925 345.309 220.102 345.309H188.511Z" fill="#0F6DFF"/>
</g>
<defs>
<clipPath id="clip0_1_2">
<rect width="800" height="800" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,53 @@
<svg width="1000" height="1000" viewBox="0 0 1000 1000" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5_59)">
<rect width="1000" height="1000" fill="white"/>
<g filter="url(#filter0_i_5_59)">
<rect width="1000" height="1000" fill="url(#paint0_linear_5_59)"/>
</g>
<g filter="url(#filter1_i_5_59)">
<path d="M291.518 763H182.984V305.363C182.984 273.132 188.52 246.186 199.592 224.526C210.665 202.867 226.243 186.364 246.328 175.018C266.67 163.673 290.746 158 318.555 158C327.568 158 336.065 158.774 344.047 160.321C352.287 161.61 360.269 163.415 367.994 165.736L366.836 251.988C363.488 250.956 359.497 250.183 354.862 249.667C350.227 249.151 345.335 248.893 340.185 248.893C329.885 248.893 321.001 251.085 313.534 255.469C306.324 259.852 300.788 266.17 296.926 274.421C293.321 282.672 291.518 292.986 291.518 305.363V763ZM417.439 341.721V423.331H132V341.721H417.439Z" fill="url(#paint1_linear_5_59)"/>
<path d="M518.223 649.985L591.223 341.721H659.201L642.593 467.038L570.752 760.217H513.974L518.223 649.985ZM497.366 341.721L547.191 649.211L546.805 760.217H480.371L393.466 341.721H497.366ZM714.048 644.183L762.328 341.721H867L779.709 760.217H713.275L714.048 644.183ZM668.471 341.721L741.471 647.664L746.492 760.217H689.715L617.873 467.811L601.651 341.721H668.471Z" fill="url(#paint2_linear_5_59)"/>
</g>
</g>
<defs>
<filter id="filter0_i_5_59" x="0" y="0" width="1000" height="1000" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="100"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0126911 0 0 0 0 0.385701 0 0 0 0 0.890053 0 0 0 0.04 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_5_59"/>
</filter>
<filter id="filter1_i_5_59" x="132" y="158" width="735" height="610" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_5_59"/>
</filter>
<linearGradient id="paint0_linear_5_59" x1="0" y1="0" x2="1000" y2="1000" gradientUnits="userSpaceOnUse">
<stop stop-color="#F0FFFA"/>
<stop offset="0.453125" stop-color="#F8F3F3"/>
<stop offset="0.583333" stop-color="#F0F0F0"/>
<stop offset="1" stop-color="#EFE4F2"/>
</linearGradient>
<linearGradient id="paint1_linear_5_59" x1="212.692" y1="233.406" x2="770.369" y2="717.58" gradientUnits="userSpaceOnUse">
<stop stop-color="#535353"/>
<stop offset="0.510417" stop-color="#0362E3"/>
<stop offset="1" stop-color="#A068BA"/>
</linearGradient>
<linearGradient id="paint2_linear_5_59" x1="212.692" y1="233.406" x2="770.369" y2="717.58" gradientUnits="userSpaceOnUse">
<stop stop-color="#535353"/>
<stop offset="0.510417" stop-color="#0362E3"/>
<stop offset="1" stop-color="#A068BA"/>
</linearGradient>
<clipPath id="clip0_5_59">
<rect width="1000" height="1000" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

View File

@@ -0,0 +1,28 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_i_5_59)">
<path d="M298.425 781H187.233V312.773C187.233 279.796 192.904 252.227 204.248 230.066C215.591 207.905 231.551 191.02 252.128 179.412C272.968 167.804 297.633 162 326.124 162C335.357 162 344.062 162.791 352.24 164.374C360.682 165.693 368.86 167.54 376.774 169.915L375.587 258.163C372.157 257.107 368.068 256.316 363.32 255.788C358.572 255.261 353.559 254.997 348.283 254.997C337.731 254.997 328.63 257.239 320.98 261.724C313.593 266.209 307.922 272.673 303.965 281.115C300.271 289.557 298.425 300.11 298.425 312.773V781ZM427.429 349.972V433.471H135V349.972H427.429Z" fill="url(#paint0_linear_5_59)"/>
<path d="M530.681 665.369L605.469 349.972H675.112L658.097 478.189L584.497 778.153H526.329L530.681 665.369ZM509.313 349.972L560.359 664.578L559.963 778.153H491.903L402.87 349.972H509.313ZM731.302 659.434L780.765 349.972H888L798.571 778.153H730.511L731.302 659.434ZM684.609 349.972L759.397 662.995L764.541 778.153H706.373L632.772 478.98L616.153 349.972H684.609Z" fill="url(#paint1_linear_5_59)"/>
</g>
<defs>
<filter id="filter0_i_5_59" x="135" y="162" width="753" height="624" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_5_59"/>
</filter>
<linearGradient id="paint0_linear_5_59" x1="217.668" y1="239.151" x2="788.355" y2="735.274" gradientUnits="userSpaceOnUse">
<stop stop-color="#535353"/>
<stop offset="0.510417" stop-color="#0362E3"/>
<stop offset="1" stop-color="#A068BA"/>
</linearGradient>
<linearGradient id="paint1_linear_5_59" x1="217.668" y1="239.151" x2="788.355" y2="735.274" gradientUnits="userSpaceOnUse">
<stop stop-color="#535353"/>
<stop offset="0.510417" stop-color="#0362E3"/>
<stop offset="1" stop-color="#A068BA"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
public/pwa/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Some files were not shown because too many files have changed in this diff Show More