- Added tone instruction retrieval to SupportReplyService. - Improved user feedback when no relevant article is found. - Included article URL and tone instruction in LLM prompt. - Updated response format to include source information. - Enhanced article management UI with search functionality and editing capabilities. - Introduced a new API endpoint for nearest articles based on vector search. - Added confidence badge component to display article confidence levels. - Implemented tests for article searching, editing, and nearest article API. - Removed obsolete .htaccess file.
129 lines
3.9 KiB
PHP
129 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Article;
|
|
use App\Models\QuickReply;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class AdminArticleService
|
|
{
|
|
public function paginate(int $perPage = 10, ?string $search = null): LengthAwarePaginator
|
|
{
|
|
$search = $search !== null ? trim($search) : '';
|
|
|
|
$numericSearch = ltrim($search, '#');
|
|
|
|
return Article::query()
|
|
->with('quickReplies')
|
|
->when($search !== '', function ($query) use ($numericSearch, $search): void {
|
|
$like = "%{$search}%";
|
|
|
|
$query->where(function ($query) use ($numericSearch, $like): void {
|
|
$query
|
|
->where('title', 'ilike', $like)
|
|
->orWhere('content', 'ilike', $like)
|
|
->orWhere('source_url', 'ilike', $like)
|
|
->orWhere('source_article_id', 'ilike', $like);
|
|
|
|
if (ctype_digit($numericSearch)) {
|
|
$query->orWhere('id', (int) $numericSearch);
|
|
}
|
|
});
|
|
})
|
|
->latest()
|
|
->paginate($perPage);
|
|
}
|
|
|
|
public function create(string $title, string $content, ?string $note = null, array $allowedActions = []): Article
|
|
{
|
|
return Article::query()->create([
|
|
'title' => trim($title),
|
|
'content' => trim($content),
|
|
'note' => $note !== null && trim($note) !== '' ? trim($note) : null,
|
|
'allowed_actions' => $this->sanitizeAllowedActions($allowedActions),
|
|
'status' => 'published',
|
|
'is_ai_draft' => false,
|
|
]);
|
|
}
|
|
|
|
public function deleteById(int $articleId): bool
|
|
{
|
|
return (bool) Article::query()->whereKey($articleId)->delete();
|
|
}
|
|
|
|
public function findById(int $articleId): ?Article
|
|
{
|
|
return Article::query()->find($articleId);
|
|
}
|
|
|
|
public function updateContent(int $articleId, string $title, string $content): bool
|
|
{
|
|
$article = Article::query()->find($articleId);
|
|
if ($article === null) {
|
|
return false;
|
|
}
|
|
|
|
$article->title = trim($title);
|
|
$article->content = trim($content);
|
|
$article->save();
|
|
|
|
return true;
|
|
}
|
|
|
|
public function updateMetadata(int $articleId, ?string $note, array $allowedActions, array $quickReplyIds = []): bool
|
|
{
|
|
$article = Article::query()->find($articleId);
|
|
if ($article === null) {
|
|
return false;
|
|
}
|
|
|
|
DB::transaction(function () use ($article, $note, $allowedActions, $quickReplyIds): void {
|
|
$article->note = $note !== null && trim($note) !== '' ? trim($note) : null;
|
|
$article->allowed_actions = $this->sanitizeAllowedActions($allowedActions);
|
|
$article->save();
|
|
$article->quickReplies()->sync($this->existingQuickReplyIds($quickReplyIds));
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
public function approveDraft(int $articleId): bool
|
|
{
|
|
$article = Article::query()->find($articleId);
|
|
if ($article === null) {
|
|
return false;
|
|
}
|
|
|
|
$article->status = 'published';
|
|
$article->is_ai_draft = false;
|
|
$article->save();
|
|
|
|
return true;
|
|
}
|
|
|
|
private function sanitizeAllowedActions(array $allowedActions): array
|
|
{
|
|
return array_values(array_intersect(array_unique($allowedActions), ['domain_inf']));
|
|
}
|
|
|
|
private function existingQuickReplyIds(array $quickReplyIds): array
|
|
{
|
|
$ids = array_values(array_unique(array_filter(
|
|
array_map(static fn ($id) => (int) $id, $quickReplyIds),
|
|
static fn ($id) => $id > 0
|
|
)));
|
|
|
|
if ($ids === []) {
|
|
return [];
|
|
}
|
|
|
|
return QuickReply::query()
|
|
->whereIn('id', $ids)
|
|
->pluck('id')
|
|
->map(static fn ($id) => (int) $id)
|
|
->all();
|
|
}
|
|
}
|