diff --git a/app/Enums/LanguageEnum.php b/app/Enums/LanguageEnum.php new file mode 100644 index 0000000..02b2439 --- /dev/null +++ b/app/Enums/LanguageEnum.php @@ -0,0 +1,9 @@ +orderBy('updated_at', 'desc') + ->limit(50) + ->get(); + + return view('the-cyber-brief.index', compact('briefes')); + } +} diff --git a/app/Models/YnhTheCyberBrief.php b/app/Models/YnhTheCyberBrief.php new file mode 100644 index 0000000..bbc0701 --- /dev/null +++ b/app/Models/YnhTheCyberBrief.php @@ -0,0 +1,155 @@ + LanguageEnum::class, + 'is_published' => 'boolean', + ]; + + public function brief(): array + { + if (!$this->teaser || !$this->opener || !$this->why_it_matters) { + + $response = $this->summary(); + + if (!isset($response['choices'][0]['message']['content'])) { + return []; + } + + $brief = $response['choices'][0]['message']['content']; + + if ($this->isHyperlink()) { + $this->hyperlink = $this->news; + $this->website = Str::before(Str::after($this->news, '://'), '/'); + } + + $this->teaser = Str::trim(Str::between($brief, '[TEASER]', '[OPENER]')); + $this->opener = Str::trim(Str::between($brief, '[OPENER]', '[WHY_IT_MATTERS]')); + + if (!Str::contains($brief, '[GO_DEEPER]')) { + $this->why_it_matters = Str::trim(Str::after($brief, '[WHY_IT_MATTERS]')); + } else { + $this->why_it_matters = Str::trim(Str::between($brief, '[WHY_IT_MATTERS]', '[GO_DEEPER]')); + $this->go_deeper = Str::trim(Str::after($brief, '[GO_DEEPER]')); + } + + $this->save(); + } + return [ + 'website' => $this->website, + 'link' => $this->hyperlink, + 'teaser' => $this->teaser, + 'opener' => $this->opener, + 'why_it_matters' => $this->why_it_matters, + 'go_deeper' => $this->go_deeper, + ]; + } + + private function summary(): array + { + if ($this->isHyperlink()) { + $news = Http::get('http://api.scraperapi.com?api_key=' . config('towerify.scraperapi.api_key') . '&url=' . $this->news); + } else { + $news = $this->news; + } + $response = Http::withHeaders([ + 'Authorization' => 'Bearer ' . config('towerify.openai.api_key'), + 'Accept' => 'application/json', + ])->post('https://api.openai.com/v1/chat/completions', [ + 'model' => 'gpt-4o', + 'messages' => [[ + 'role' => 'user', + 'content' => $this->prompt($news) + ]], + 'temperature' => 0.7 + ]); + if ($response->successful()) { + $json = $response->json(); + // Log::debug($json); + return $json; + } + Log::error($response->body()); + return []; + } + + private function prompt(string $news): string + { + return " +Below is the description of SmartBrevity's four parts news format: +[TEASER] Six or fewer strong words to catch someone's attention. +[OPENER] A single sentence that should tell me something I don't know, would want to know or should know. Make this sentence as direct, short and sharp as possible. +[WHY_IT_MATTERS] A few sentences or bullet points to explain why this new fact, idea or though matters. +[GO_DEEPER] A few sentences or paragraphs that expand on 'Why it matters' with greater details. + +Be aware that: +- These four parts must fit in one screen of phone, regardless of what it is. +- The [TEASER], [OPENER] and [WHY_IT_MATTERS] parts are mandatory. +- The [GO_DEEPER] part is optional. +- Do not use Markdown. + +Below is an example of an original news rewritten using SmartBrevity's news format: +- Original news: + Title: Hey, there are some new plans for the weekend to discuss re: birthday party. + Sorry for the late change of plans but there's been so much chaos in pulling Jimmi's party together especially with the weather this past week. The good news is we found a place to take all of the kids, that new trampoline park. We will do this Saturday at noon. + The only hitch is it's a little farther than we originally planned. The first spot we were looking at was 30-minute drive, but trampoline park has a lot more space, so we picked it even though it's about 40 minutes away. Just flagging for planning purpose. + The place is located at 1100 Wilson Street by that sushi restaurant we visited that had those awesome spider rolls. Ha ha. It starts at noon, and our session ends at 4 pm. Feel free to stay or go since we have the instructor and we will serve lunch and drinks. I will stay and read or worry. They should dress to play! Shorts and shirts, oh and socks requires... see you soon and sorry again. +- Rewritten news: + [TEASER] New plan: trampoline park. + [OPENER] We're moving Jimmy's party to the new trampoline park this Saturday at noon. + [WHY_IT_MATTERS] It's about 40-minute drive, so you might need to leave a little earlier than we first thought. + [GO_DEEPER] + • Arrive @ noon, 1100 Wilson Street. + • Pizza & drinks provided. + • Pick up kids @ 4pm. + • Dress to play. Socks REQUIRED. + +Below is another example of an original news rewritten using SmartBrevity's news format: +- Original news: + Title: Board of Directors Update + We presented on our progress toward our go-to-market plan in our most recent Board of Directors meeting, Wednesday, including strong product sales over the last quarter within the scope of our beta test. We were able to “wow” the Board with a report including a 12 percent jump in revenue over the last quarter, which puts us an extraordinary 90 percent of the way to our overall goal for the second half. + Strong product sales will allow us to increase investment in key early growth opportunities across tech and marketing. We’re updating the second-half roadmap with big investments in the tech team, particularly on the machine learning squad, marketing, to support Ava’s team with our new pitch and positioning, and in some exciting new collaborations with firms doing work where we don’t have internal capacity but do have a strategic need to add expertise. + If you haven’t taken time to review Ava’s new pitch and positioning documents, we encourage everyone to do so. The new talking points went through a lot of testing with focus groups and reflect our best argument to date on why our solution is the best in the industry. +- Rewritten news: + [TEASER] We wowed our Board + [OPENER] We stunned the Board Wednesday with Q3’s 12% revenue jump, putting us 90% to goal for H2. + [WHY_IT_MATTERS] Higher revenue means we can invest in two areas that will speed up our go-to-market plan by months. + • New hires: We can add key machine learning roles on the tech and marketing teams. + • Partnerships: We’ll finalize two deals to expand our skills and strategic thinking. + [GO_DEEPER] Our product speaks for itself, but it was Ava’s new pitch—tested over three weeks of focus groups—that got it into customers’ hands. + • Please review Ava’s materials on the intranet. + +Now, take the following text and summarizes it using SmartBrevity's news format: {$news} + "; + } + + private function isHyperlink(): bool + { + return Str::startsWith(Str::lower($this->news), ["https://", "http://"]); + } +} diff --git a/config/towerify.php b/config/towerify.php index 0ab4f6b..577ef94 100644 --- a/config/towerify.php +++ b/config/towerify.php @@ -20,4 +20,10 @@ 'username' => env('ADMIN_USERNAME'), 'password' => env('ADMIN_PASSWORD'), ], + 'openai' => [ + 'api_key' => env('OPENAI_API_KEY'), + ], + 'scraperapi' => [ + 'api_key' => env('SCRAPERAPI_API_KEY'), + ], ]; diff --git a/database/migrations/2024_08_14_165202_create_table_ynh_cybernews.php b/database/migrations/2024_08_14_165202_create_table_ynh_cybernews.php new file mode 100644 index 0000000..488a8df --- /dev/null +++ b/database/migrations/2024_08_14_165202_create_table_ynh_cybernews.php @@ -0,0 +1,50 @@ +id(); + $table->timestamps(); + + // The news source and language + $table->text('news'); + $table->enum('news_language', [ + \App\Enums\LanguageEnum::ENGLISH->value, + \App\Enums\LanguageEnum::FRENCH->value, + ])->default('fr'); + + // The news summary + $table->string('hyperlink')->nullable(); + $table->string('website')->nullable(); + $table->string('teaser', 140)->nullable(); // old twitter + $table->string('opener', 280)->nullable(); // new twitter + $table->string('why_it_matters', 1000)->nullable(); + $table->text('go_deeper')->nullable(); + + // The news status + $table->boolean('is_published')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('ynh_the_cyber_brief'); + } +}; diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 85f792c..d2fd8dd 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -112,9 +112,6 @@
- @guest - - @else
@@ -132,7 +129,6 @@ @include('flash::message')
- @endguest @yield('content')
diff --git a/resources/views/the-cyber-brief/index.blade.php b/resources/views/the-cyber-brief/index.blade.php new file mode 100644 index 0000000..fd469a4 --- /dev/null +++ b/resources/views/the-cyber-brief/index.blade.php @@ -0,0 +1,186 @@ +@extends('layouts.app') + +@section('content') + +
+
+
+ @if($briefes->isEmpty()) + {{ __('All clear—no briefs today!') }} + @else +
+ COMPACT   + +
+ @foreach ($briefes as $brief) +
+
+
+ > {{ strtoupper($brief->brief()['teaser']) }} +
+
+ {{ $brief->brief()['opener'] }} +
+ @if($brief->brief()['why_it_matters']) +
+ brief()['why_it_matters']) ?> + @foreach($whyItMatters as $index => $text) + @if($index === 0) +
+ WHY IT MATTERS  +
+ @endif +
+ {{ trim($text) }} +
+ @endforeach +
+ @endif + @if($brief->brief()['go_deeper']) +
+ brief()['go_deeper']) ?> + @foreach($goDeeper as $index => $text) + @if($index === 0) +
+ GO DEEPER  +
+ @endif +
+ {{ trim($text) }} +
+ @endforeach +
+ @endif + @if($brief->brief()['website']) + + @endif +
+
+ @endforeach + @endif +
+
+
+ +@endsection diff --git a/routes/web.php b/routes/web.php index 634780b..1e79514 100644 --- a/routes/web.php +++ b/routes/web.php @@ -237,20 +237,22 @@ if (\Illuminate\Support\Facades\Auth::user()) { return redirect('/home'); } - return redirect('/shop/index'); + return redirect('/the-cyber-brief'); }); Route::get('/', function () { if (\Illuminate\Support\Facades\Auth::user()) { return redirect('/home'); } - return redirect('/shop/index'); + return redirect('/the-cyber-brief'); }); Auth::routes(); Route::get('/home', 'HomeController@index')->name('home'); +Route::get('/the-cyber-brief', 'TheCyberBriefController@index')->name('the-cyber-brief'); + Route::post('/reset-password', function () { $email = \Illuminate\Support\Facades\Auth::user()->email; return view('auth.passwords.email', compact('email'));