Skip to content

Commit 0020b7b

Browse files
committed
Centralize search result processing and initial chart data generation in a new backend controller method.
1 parent 1390f2c commit 0020b7b

3 files changed

Lines changed: 87 additions & 54 deletions

File tree

app/Http/Controllers/Api/ChatV2Controller.php

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,45 @@ public function index(): Response
2828
return Inertia::render('ChatV2/Index');
2929
}
3030

31+
public function search(Request $request): Response
32+
{
33+
if ($request->user()) {
34+
$request->user()->checkAndResetDailyCredits();
35+
}
36+
37+
$q = $request->query('q');
38+
$initialChartData = null;
39+
40+
if (!empty($q)) {
41+
$results = $this->searchService->searchTerms($q, false, true);
42+
43+
if (!empty($results) && !isset($results['is_composition_fallback'])) {
44+
$topGroup = $results[0];
45+
46+
if (isset($topGroup['global_stats']) && count($topGroup['global_stats']) > 0) {
47+
$chartDataRows = [];
48+
foreach ($topGroup['global_stats'] as $stat) {
49+
$chartDataRows[] = [
50+
'name' => $stat['term'],
51+
'value' => $stat['total_count']
52+
];
53+
}
54+
55+
$initialChartData = [
56+
'type' => 'bar',
57+
'title' => 'توزيع انتشار التراجم (' . ($topGroup['display_term_en'] ?? $q) . ')',
58+
'data' => array_slice($chartDataRows, 0, 10)
59+
];
60+
}
61+
}
62+
}
63+
64+
return Inertia::render('ChatV2/Results', [
65+
'q' => $q,
66+
'initialChartData' => $initialChartData
67+
]);
68+
}
69+
3170
public function chat(Request $request)
3271
{
3372
$request->validate([
@@ -101,32 +140,18 @@ public function chat(Request $request)
101140
(CRITICAL INSTRUCTION: Look at the top-level `resource_count` field in the JSON data. DO NOT calculate counts yourself. DO NOT mention how many times the term appeared in total, ONLY mention the number of sources/resources! If 0 results, put 0 and explain from your knowledge base contextually.)
102141
**التعريف:** [Brief definition of the English term in Arabic to set context]
103142
104-
## 1. الإحصائيات المرئية (Visual Statistics)
105-
You MUST generate a visual chart to show the frequency of the top Arabic translations FIRST. YOU MUST WRAP the JSON data inside a markdown code block starting with ` ```recharts ` and ending with ` ``` `.
106-
107-
```recharts
108-
{
109-
\"type\": \"bar\",
110-
\"title\": \"توزيع انتشار التراجم\",
111-
\"data\": [
112-
{\"name\": \"[Arabic Term 1]\", \"value\": [total_count]}
113-
]
114-
}
115-
```
116-
**CRITICAL:** DO NOT FORGET the ```recharts and ``` tags around the JSON!
117-
118-
## 2. ملخص الاستعمال الأكثر شيوعاً
143+
## 1. ملخص الاستعمال الأكثر شيوعاً
119144
[Provide a summary of the most used Arabic term and its acceptance level. Do not mention total frequency numbers, focus on the variety of sources.]
120145
121-
## 3. التحليل التفصيلي حسب المصدر
146+
## 2. التحليل التفصيلي حسب المصدر
122147
[List each resource and the term it uses. CRITICAL: You MUST format every page number as a standard markdown clickable link EXACTLY using this format: `[ص. X](/resources/{resource_id}/pdf#page={page_number})`.
123148
Example of CORRECT syntax: `[ص. 5](/resources/10/pdf#page=5)`
124149
Example of INCORRECT syntax: `(/resources/10/pdf#page=5)[ص. 5]` — NEVER do this!]
125150
126-
## 4. التحليل اللغوي والدلالي (Linguistic and Semantic Analysis)
151+
## 3. التحليل اللغوي والدلالي (Linguistic and Semantic Analysis)
127152
[Provide a deep linguistic and semantic description for the most used term. Discuss its linguistic roots, morphology (e.g., verb weights, derivation like 'افتعل'), and the semantic nuances that make it an appropriate translation for the English term, similar to how Arabic language academies analyze terms.]
128153
129-
## 5. تقييم وانتقاد الترجمة (Criticism and Evaluation)
154+
## 4. تقييم وانتقاد الترجمة (Criticism and Evaluation)
130155
[Provide a detailed, objective evaluation and criticism (إنتقاد) of the most used Arabic translation. Discuss its shortcomings, whether it truly captures the technical essence of the English Computer Science term, any potential confusion it might cause, and if there are better or more precise alternatives, even if less commonly used.]
131156
132157
Note: Be professional and comprehensive. Ensure every cited page has a link.";
@@ -404,4 +429,4 @@ public function downloadPdf(Request $request) {
404429
'Content-Disposition' => 'attachment; filename="term_report_' . time() . '.pdf"',
405430
]);
406431
}
407-
}
432+
}

resources/js/Pages/ChatV2/Results.jsx

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useRef } from "react";
2-
import { Head, Link, usePage } from "@inertiajs/react";
2+
import { Head, Link, usePage, router } from "@inertiajs/react";
33
import { Button } from "@/Components/ui/button";
44
import { Switch } from "@/Components/ui/switch";
55
import {
@@ -12,14 +12,14 @@ import ReactMarkdown from "react-markdown";
1212
import remarkGfm from "remark-gfm";
1313
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell, PieChart, Pie, Legend } from 'recharts';
1414

15-
export default function Results({ q }) {
15+
export default function Results({ q, initialChartData }) {
1616
const [query, setQuery] = useState(q || "");
1717
const [result, setResult] = useState("");
1818
const [loading, setLoading] = useState(false);
1919
const [error, setError] = useState(null);
2020
const [detailedMode, setDetailedMode] = useState(false);
2121
const { auth } = usePage().props;
22-
const initialSearchDone = useRef(false);
22+
const initialSearchDone = useRef(null);
2323

2424
// Local state for credits to update immediately without waiting for page reload
2525
const [credits, setCredits] = useState(auth.user?.daily_credits ?? 0);
@@ -74,22 +74,26 @@ export default function Results({ q }) {
7474
return { chartData: null, cleanText: text };
7575
};
7676

77-
const { chartData, cleanText } = extractChartContent(result);
77+
const { chartData: extractedChartData, cleanText } = extractChartContent(result);
78+
// Use the backend-provided chart if available, otherwise fallback to extracted (if AI generated it)
79+
const chartData = initialChartData || extractedChartData;
7880

7981
useEffect(() => {
80-
if (q && !initialSearchDone.current) {
81-
initialSearchDone.current = true;
82+
if (q && initialSearchDone.current !== q) {
83+
initialSearchDone.current = q;
8284
performSearch(q);
8385
}
8486
}, [q]);
8587

86-
const performSearch = async (textToSearch) => {
88+
const performSearch = async (textToSearch, modeOverride = undefined) => {
8789
if (!textToSearch.trim() || loading) return;
8890

8991
setLoading(true);
9092
setResult("");
9193
setError(null);
9294

95+
const isDetailed = modeOverride !== undefined ? modeOverride : detailedMode;
96+
9397
try {
9498
// Read updated X-XSRF-TOKEN from cookies to prevent 419 errors after login navigations
9599
const getXsrfToken = () => {
@@ -106,7 +110,7 @@ export default function Results({ q }) {
106110
},
107111
body: JSON.stringify({
108112
messages: [{ role: "user", content: textToSearch }],
109-
detailed_mode: detailedMode
113+
detailed_mode: isDetailed
110114
}),
111115
});
112116

@@ -138,16 +142,21 @@ export default function Results({ q }) {
138142
if (!line.trim()) continue;
139143
try {
140144
const json = JSON.parse(line);
141-
if (json.chunk) {
142-
accumulatedContent += json.chunk;
143-
let displayContent = accumulatedContent;
144-
displayContent = displayContent.replace(/<think>[\s\S]*?<\/think>/gi, "");
145-
displayContent = displayContent.replace(/<[\|][\s\S]*?[\|]>/gu, "");
146-
displayContent = displayContent.replace(/<dsml>[\s\S]*?<\/dsml>/gi, "");
147-
if (displayContent.match(/^<think>/i)) displayContent = "";
148-
displayContent = displayContent.replace(/^Thinking\.\.\.\s*/i, "");
149-
setResult(displayContent.trim());
150-
}
145+
if (json.chunk) {
146+
accumulatedContent += json.chunk;
147+
let displayContent = accumulatedContent;
148+
displayContent = displayContent.replace(/<think>[\s\S]*?<\/think>/gi, "");
149+
displayContent = displayContent.replace(/<[\|].*?[\|]>/gu, "");
150+
displayContent = displayContent.replace(/<[\/]?([\|]DSML[\|])[^>]*>/gi, "");
151+
displayContent = displayContent.replace(/<dsml>[\s\S]*?<\/dsml>/gi, "");
152+
displayContent = displayContent.replace(/Let me try a different approach to search for this term\./gi, "");
153+
displayContent = displayContent.replace(/Let me search for this term\./gi, "");
154+
displayContent = displayContent.replace(/Let me look up this term\./gi, "");
155+
if (displayContent.match(/^<think>/i)) displayContent = "";
156+
displayContent = displayContent.replace(/^Thinking\.\.\.\s*/i, "");
157+
displayContent = displayContent.trim().replace(/^_+/g, '').trim();
158+
setResult(displayContent);
159+
}
151160
} catch (e) {}
152161
}
153162
}
@@ -166,13 +175,13 @@ export default function Results({ q }) {
166175
const handleSearchSubmit = (e) => {
167176
e.preventDefault();
168177

169-
// Update URL if changed
170-
const currentParams = new URLSearchParams(window.location.search);
171-
if (currentParams.get('q') !== query) {
172-
window.history.pushState({}, '', `/search?q=${encodeURIComponent(query)}`);
173-
}
174-
175-
performSearch(query);
178+
if (!query.trim() || loading) return;
179+
180+
// Perform an Inertia visit to fetch new props (like initialChartData) from backend
181+
router.get(`/search`, { q: query }, {
182+
preserveState: true,
183+
preserveScroll: true
184+
});
176185
};
177186

178187
return (
@@ -269,14 +278,20 @@ export default function Results({ q }) {
269278
</div>
270279

271280
<div
272-
onClick={() => setDetailedMode(!detailedMode)}
281+
onClick={() => {
282+
const newMode = !detailedMode;
283+
setDetailedMode(newMode);
284+
if (query) {
285+
performSearch(query, newMode);
286+
}
287+
}}
273288
className="flex items-center gap-1.5 md:gap-2 bg-white px-2.5 py-1.5 md:px-3 md:py-2 rounded-xl border border-slate-200 shadow-sm cursor-pointer select-none hover:bg-slate-50 transition-colors shrink-0"
274289
>
275290
<span className="text-[10px] md:text-xs font-bold text-slate-600 flex items-center gap-1 md:gap-1.5">
276291
<FileText className={`h-3 w-3 md:h-3.5 md:w-3.5 ${detailedMode ? 'text-blue-500' : 'text-slate-400'}`} />
277292
<span>وضع تفصيلي</span>
278293
</span>
279-
<Switch checked={detailedMode} onCheckedChange={setDetailedMode} className="scale-[0.65] md:scale-75 data-[state=checked]:bg-blue-600 pointer-events-none origin-left rtl:origin-right" />
294+
<Switch checked={detailedMode} readOnly className="scale-[0.65] md:scale-75 data-[state=checked]:bg-blue-600 pointer-events-none origin-left rtl:origin-right" />
280295
</div>
281296
</div>
282297

routes/web.php

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,7 @@
1717
return inertia("ChatV2/Changelog");
1818
})->name("changelog");
1919

20-
Route::get("/search", function (\Illuminate\Http\Request $request) {
21-
if ($request->user()) {
22-
$request->user()->checkAndResetDailyCredits();
23-
}
24-
return inertia("ChatV2/Results", [
25-
'q' => $request->query('q')
26-
]);
27-
})->middleware(['auth', 'approved'])->name("search.results");
20+
Route::get("/search", [ChatV2Controller::class, "search"])->middleware(['auth', 'approved'])->name("search.results");
2821

2922
Route::get("/thanks", function () {
3023
return inertia("Thanks");

0 commit comments

Comments
 (0)