Skip to content

Commit 8b75ea9

Browse files
dbym4820claude
andcommitted
v1.2.0 - PDF support for AI summaries and trend filtering
Features: - AI summaries now use PDF when available (Claude), with fallback to HTML/abstract - AI chat also prioritizes PDF input for better context - PDF viewer embedded in paper card for papers with pdf_url - Trend summaries can be filtered by tags - Trend summary history with ability to view past summaries - Summary history button only shows when 2+ summaries exist - Fixed summary created_at timestamp Technical changes: - Added pdf_url column to papers table - Added user_id and tag_ids to trend_summaries for history tracking - AiSummaryService: callClaudeWithPdf(), callClaudeChatWithPdf() methods - TrendController: tag filtering and history API - New API endpoint: GET /api/trends/history 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 1fe662c commit 8b75ea9

File tree

19 files changed

+1153
-105
lines changed

19 files changed

+1153
-105
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,10 @@ DEFAULT_JOURNALS=
4242
ADMIN_CLAUDE_API_KEY=
4343
ADMIN_OPENAI_API_KEY=
4444

45+
# Unpaywall API(本文取得用)
46+
# DOIがある論文のPDF/本文を自動取得するために必要
47+
# メールアドレスはAPI利用の識別に使用(認証ではない)
48+
UNPAYWALL_EMAIL=
49+
4550
# デフォルト要約テンプレート・調査観点設定
4651
# config/generative_ai_settings/*.txt で管理しています(Git管理可能)

app/Console/Commands/FetchFullText.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public function handle(): int
6868
$paper->update([
6969
'full_text' => $result['text'],
7070
'full_text_source' => $result['source'],
71+
'pdf_url' => $result['pdf_url'],
7172
'full_text_fetched_at' => now(),
7273
]);
7374
$success++;

app/Http/Controllers/PaperController.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ public function getFullText(Request $request, int $id): JsonResponse
115115
'title' => $paper->title,
116116
'full_text' => $paper->full_text,
117117
'full_text_source' => $paper->full_text_source,
118+
'pdf_url' => $paper->pdf_url,
118119
'full_text_fetched_at' => $paper->full_text_fetched_at?->toISOString(),
119120
]);
120121
}
@@ -145,6 +146,7 @@ private function formatPaper(Paper $paper, bool $detailed = false): array
145146
'has_summary' => $paper->summaries->count() > 0 ? 1 : 0,
146147
'has_full_text' => $paper->hasFullText(),
147148
'full_text_source' => $paper->full_text_source,
149+
'pdf_url' => $paper->pdf_url,
148150
// Always include summaries for frontend to show existing summaries
149151
'summaries' => $paper->summaries->map(function ($s) {
150152
return [

app/Http/Controllers/SummaryController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public function generate(Request $request): JsonResponse
9696
'implications' => $result['implications'] ?? null,
9797
'tokens_used' => $result['tokens_used'] ?? null,
9898
'generation_time_ms' => $result['generation_time_ms'] ?? null,
99+
'created_at' => now(),
99100
]);
100101

101102
return response()->json([

app/Http/Controllers/TrendController.php

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,24 @@ public function generate(Request $request, string $period): JsonResponse
7474
return response()->json(['error' => '無効な期間です'], 400);
7575
}
7676

77-
$papers = Paper::with('journal:id,name')
77+
// Get tag IDs from request (optional)
78+
$tagIds = $request->input('tagIds', []);
79+
if (!is_array($tagIds)) {
80+
$tagIds = [];
81+
}
82+
83+
$papersQuery = Paper::with(['journal:id,name', 'tags:id,name'])
7884
->forUser($user->id)
79-
->whereBetween('published_date', [$dateRange['from'], $dateRange['to']])
80-
->orderBy('published_date', 'desc')
81-
->get();
85+
->whereBetween('published_date', [$dateRange['from'], $dateRange['to']]);
86+
87+
// Filter by tags if specified
88+
if (!empty($tagIds)) {
89+
$papersQuery->whereHas('tags', function ($query) use ($tagIds) {
90+
$query->whereIn('tags.id', $tagIds);
91+
});
92+
}
93+
94+
$papers = $papersQuery->orderBy('published_date', 'desc')->get();
8295

8396
if ($papers->isEmpty()) {
8497
return response()->json([
@@ -98,7 +111,7 @@ public function generate(Request $request, string $period): JsonResponse
98111
$provider = $request->input('provider', config('services.ai.provider', 'claude'));
99112

100113
try {
101-
$summary = $this->generateTrendSummary($papers, $period, $dateRange, $provider);
114+
$summary = $this->generateTrendSummary($user->id, $papers, $period, $dateRange, $provider, $tagIds);
102115

103116
return response()->json([
104117
'success' => true,
@@ -109,6 +122,7 @@ public function generate(Request $request, string $period): JsonResponse
109122
],
110123
'paperCount' => $papers->count(),
111124
'provider' => $provider,
125+
'tagIds' => $tagIds,
112126
'summary' => $summary,
113127
]);
114128
} catch (\Exception $e) {
@@ -123,6 +137,8 @@ public function generate(Request $request, string $period): JsonResponse
123137
*/
124138
public function summary(Request $request, string $period): JsonResponse
125139
{
140+
$user = $request->attributes->get('user');
141+
126142
$dateRange = $this->getDateRange($period);
127143

128144
if (!$dateRange) {
@@ -132,8 +148,14 @@ public function summary(Request $request, string $period): JsonResponse
132148
$dateFrom = $dateRange['from']->format('Y-m-d');
133149
$dateTo = $dateRange['to']->format('Y-m-d');
134150

135-
// Try to get from database
136-
$savedSummary = TrendSummary::findByPeriodAndDate($period, $dateFrom, $dateTo);
151+
// Get tag IDs from query string (optional)
152+
$tagIds = $request->query('tagIds', []);
153+
if (is_string($tagIds)) {
154+
$tagIds = $tagIds ? array_map('intval', explode(',', $tagIds)) : [];
155+
}
156+
157+
// Try to get latest summary for user with matching tags
158+
$savedSummary = TrendSummary::findLatestForUser($user->id, $period, $tagIds ?: null);
137159

138160
if ($savedSummary) {
139161
return response()->json([
@@ -147,6 +169,7 @@ public function summary(Request $request, string $period): JsonResponse
147169
'provider' => $savedSummary->ai_provider,
148170
'model' => $savedSummary->ai_model,
149171
'paperCount' => $savedSummary->paper_count,
172+
'tagIds' => $savedSummary->tag_ids ?? [],
150173
'summary' => $savedSummary->toApiResponse(),
151174
]);
152175
}
@@ -163,6 +186,22 @@ public function summary(Request $request, string $period): JsonResponse
163186
]);
164187
}
165188

189+
/**
190+
* Get trend summary history for a user
191+
*/
192+
public function history(Request $request): JsonResponse
193+
{
194+
$user = $request->attributes->get('user');
195+
$limit = (int) $request->query('limit', 20);
196+
197+
$summaries = TrendSummary::getHistoryForUser($user->id, $limit);
198+
199+
return response()->json([
200+
'success' => true,
201+
'summaries' => $summaries->map(fn($s) => $s->toApiResponse()),
202+
]);
203+
}
204+
166205
/**
167206
* Get statistics for all periods
168207
*/
@@ -225,7 +264,7 @@ private function getDateRange(string $period): ?array
225264
/**
226265
* Generate trend summary using AI
227266
*/
228-
private function generateTrendSummary($papers, string $period, array $dateRange, string $provider = 'claude'): array
267+
private function generateTrendSummary(int $userId, $papers, string $period, array $dateRange, string $provider = 'claude', array $tagIds = []): array
229268
{
230269
$periodLabels = [
231270
'day' => '今日',
@@ -254,11 +293,12 @@ private function generateTrendSummary($papers, string $period, array $dateRange,
254293

255294
$result = $this->aiService->generateCustomSummary($prompt, $provider);
256295

257-
// Save to database
296+
// Save to database (always create new record for history)
258297
$dateFrom = $dateRange['from']->format('Y-m-d');
259298
$dateTo = $dateRange['to']->format('Y-m-d');
260299

261-
TrendSummary::createOrUpdateSummary([
300+
TrendSummary::createSummary([
301+
'user_id' => $userId,
262302
'period' => $period,
263303
'date_from' => $dateFrom,
264304
'date_to' => $dateTo,
@@ -270,6 +310,7 @@ private function generateTrendSummary($papers, string $period, array $dateRange,
270310
'journal_insights' => $result['journalInsights'] ?? null,
271311
'recommendations' => $result['recommendations'] ?? null,
272312
'paper_count' => $papers->count(),
313+
'tag_ids' => !empty($tagIds) ? $tagIds : null,
273314
]);
274315

275316
return $result;

app/Models/Paper.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Paper extends Model
1717
'abstract',
1818
'full_text',
1919
'full_text_source',
20+
'pdf_url',
2021
'full_text_fetched_at',
2122
'url',
2223
'doi',

app/Models/Summary.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Summary extends Model
2020
'implications',
2121
'tokens_used',
2222
'generation_time_ms',
23+
'created_at',
2324
];
2425

2526
protected $casts = [

app/Models/TrendSummary.php

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
namespace App\Models;
44

55
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
67

78
class TrendSummary extends Model
89
{
910
protected $fillable = [
11+
'user_id',
1012
'period',
1113
'date_from',
1214
'date_to',
@@ -18,6 +20,7 @@ class TrendSummary extends Model
1820
'journal_insights',
1921
'recommendations',
2022
'paper_count',
23+
'tag_ids',
2124
];
2225

2326
protected $casts = [
@@ -27,10 +30,61 @@ class TrendSummary extends Model
2730
'emerging_trends' => 'array',
2831
'journal_insights' => 'array',
2932
'recommendations' => 'array',
33+
'tag_ids' => 'array',
3034
];
3135

3236
/**
33-
* Find summary by period and date range
37+
* Get the user that owns this trend summary
38+
*/
39+
public function user(): BelongsTo
40+
{
41+
return $this->belongsTo(User::class);
42+
}
43+
44+
/**
45+
* Find latest summary by period for a user
46+
*
47+
* @param int $userId
48+
* @param string $period
49+
* @param array|null $tagIds
50+
* @return TrendSummary|null
51+
*/
52+
public static function findLatestForUser(int $userId, string $period, ?array $tagIds = null)
53+
{
54+
$query = self::where('user_id', $userId)
55+
->where('period', $period)
56+
->orderBy('created_at', 'desc');
57+
58+
if ($tagIds !== null && count($tagIds) > 0) {
59+
// JSONカラムで完全一致検索
60+
$query->whereJsonContains('tag_ids', $tagIds);
61+
} else {
62+
$query->where(function ($q) {
63+
$q->whereNull('tag_ids')
64+
->orWhere('tag_ids', '[]');
65+
});
66+
}
67+
68+
return $query->first();
69+
}
70+
71+
/**
72+
* Get history for a user
73+
*
74+
* @param int $userId
75+
* @param int $limit
76+
* @return \Illuminate\Database\Eloquent\Collection
77+
*/
78+
public static function getHistoryForUser(int $userId, int $limit = 20)
79+
{
80+
return self::where('user_id', $userId)
81+
->orderBy('created_at', 'desc')
82+
->limit($limit)
83+
->get();
84+
}
85+
86+
/**
87+
* Find summary by period and date range (legacy, for backwards compatibility)
3488
*
3589
* @param string $period
3690
* @param string $dateFrom
@@ -42,34 +96,33 @@ public static function findByPeriodAndDate(string $period, string $dateFrom, str
4296
return self::where('period', $period)
4397
->where('date_from', $dateFrom)
4498
->where('date_to', $dateTo)
99+
->orderBy('created_at', 'desc')
45100
->first();
46101
}
47102

48103
/**
49-
* Create or update summary
104+
* Create a new summary (always creates new record for history)
50105
*
51106
* @param array $data
52107
* @return TrendSummary
53108
*/
54-
public static function createOrUpdateSummary(array $data)
109+
public static function createSummary(array $data)
55110
{
56-
return self::updateOrCreate(
57-
[
58-
'period' => $data['period'],
59-
'date_from' => $data['date_from'],
60-
'date_to' => $data['date_to'],
61-
],
62-
[
63-
'ai_provider' => $data['ai_provider'],
64-
'ai_model' => $data['ai_model'] ?? null,
65-
'overview' => $data['overview'] ?? null,
66-
'key_topics' => $data['key_topics'] ?? null,
67-
'emerging_trends' => $data['emerging_trends'] ?? null,
68-
'journal_insights' => $data['journal_insights'] ?? null,
69-
'recommendations' => $data['recommendations'] ?? null,
70-
'paper_count' => $data['paper_count'] ?? 0,
71-
]
72-
);
111+
return self::create([
112+
'user_id' => $data['user_id'] ?? null,
113+
'period' => $data['period'],
114+
'date_from' => $data['date_from'],
115+
'date_to' => $data['date_to'],
116+
'ai_provider' => $data['ai_provider'],
117+
'ai_model' => $data['ai_model'] ?? null,
118+
'overview' => $data['overview'] ?? null,
119+
'key_topics' => $data['key_topics'] ?? null,
120+
'emerging_trends' => $data['emerging_trends'] ?? null,
121+
'journal_insights' => $data['journal_insights'] ?? null,
122+
'recommendations' => $data['recommendations'] ?? null,
123+
'paper_count' => $data['paper_count'] ?? 0,
124+
'tag_ids' => $data['tag_ids'] ?? null,
125+
]);
73126
}
74127

75128
/**
@@ -80,11 +133,20 @@ public static function createOrUpdateSummary(array $data)
80133
public function toApiResponse(): array
81134
{
82135
return [
136+
'id' => $this->id,
83137
'overview' => $this->overview,
84138
'keyTopics' => $this->key_topics ?? [],
85139
'emergingTrends' => $this->emerging_trends ?? [],
86140
'journalInsights' => $this->journal_insights ?? [],
87141
'recommendations' => $this->recommendations ?? [],
142+
'period' => $this->period,
143+
'dateFrom' => $this->date_from?->format('Y-m-d'),
144+
'dateTo' => $this->date_to?->format('Y-m-d'),
145+
'paperCount' => $this->paper_count,
146+
'tagIds' => $this->tag_ids ?? [],
147+
'provider' => $this->ai_provider,
148+
'model' => $this->ai_model,
149+
'createdAt' => $this->created_at?->toISOString(),
88150
];
89151
}
90152
}

0 commit comments

Comments
 (0)