Add web research and source file reading

This commit is contained in:
Hamza Ayed
2026-10-01 14:06:36 +03:00
parent 2d3778a92e
commit dc4d5bad0f
9 changed files with 310 additions and 23 deletions
@@ -205,6 +205,45 @@ class ApiRepository {
}
}
Future<String> searchWeb(String query, {String? model}) async {
final client = http.Client();
_activeRequestClient = client;
try {
final request = http.Request(
'POST',
Uri.parse('$_baseUrl/v1/web/search'),
)
..headers['Content-Type'] = 'application/json'
..body = jsonEncode({
'query': query,
'max_results': 5,
if (model != null) 'model': model,
});
final streamed = await client
.send(request)
.timeout(const Duration(minutes: 10));
final response = await http.Response.fromStream(streamed);
_checkStatus(response);
final data = jsonDecode(response.body) as Map<String, dynamic>;
final result = data['result'] as String? ?? '';
final sources = (data['sources'] as List<dynamic>? ?? const [])
.cast<Map<String, dynamic>>();
if (sources.isEmpty) return result;
final sourceList = sources.map((source) {
final index = source['index'];
final title = source['title'] as String? ?? 'مصدر';
final url = source['url'] as String? ?? '';
final status = source['status'] as String?;
final note = status == 'snippet_only' ? ' (مقتطف فقط؛ تعذر فتح الصفحة)' : '';
return '[$index] [$title]($url)$note';
}).join('\n');
return '$result\n\nالمصادر:\n$sourceList';
} finally {
if (identical(_activeRequestClient, client)) _activeRequestClient = null;
client.close();
}
}
Stream<String> completeStream(
List<ChatMessage> messages, {
String? model,
@@ -88,6 +88,19 @@ class ChatCubit extends Cubit<ChatState> {
emit(
state.copyWith(
isWorkspaceMode: !state.isWorkspaceMode,
isWebSearchMode: false,
clearError: true,
),
);
}
}
void toggleWebSearchMode() {
if (!state.isSending) {
emit(
state.copyWith(
isWebSearchMode: !state.isWebSearchMode,
isWorkspaceMode: false,
clearError: true,
),
);
@@ -254,11 +267,11 @@ class ChatCubit extends Cubit<ChatState> {
),
);
_startElapsedTimer(generation);
if (state.isWorkspaceMode) {
answer = await _api.askWorkspaceAgent(
prompt,
model: state.selectedModel,
);
if (state.isWorkspaceMode || state.isWebSearchMode) {
answer =
state.isWorkspaceMode
? await _api.askWorkspaceAgent(prompt, model: state.selectedModel)
: await _api.searchWeb(prompt, model: state.selectedModel);
if (isClosed || generation != _requestGeneration) return;
if (answer.trim().isEmpty) {
throw Exception('لم يصل نص في إجابة الوكيل.');
@@ -81,6 +81,7 @@ class ChatState {
this.isLoadingHistory = true,
this.isSending = false,
this.isWorkspaceMode = false,
this.isWebSearchMode = false,
this.error,
this.modelName = 'جارٍ اكتشاف النموذج…',
this.modelStatus = ModelConnectionStatus.checking,
@@ -98,6 +99,7 @@ class ChatState {
final bool isLoadingHistory;
final bool isSending;
final bool isWorkspaceMode;
final bool isWebSearchMode;
final String? error;
final String modelName;
final ModelConnectionStatus modelStatus;
@@ -116,6 +118,7 @@ class ChatState {
bool? isLoadingHistory,
bool? isSending,
bool? isWorkspaceMode,
bool? isWebSearchMode,
String? error,
bool clearError = false,
String? modelName,
@@ -138,6 +141,7 @@ class ChatState {
isLoadingHistory: isLoadingHistory ?? this.isLoadingHistory,
isSending: isSending ?? this.isSending,
isWorkspaceMode: isWorkspaceMode ?? this.isWorkspaceMode,
isWebSearchMode: isWebSearchMode ?? this.isWebSearchMode,
error: clearError ? null : (error ?? this.error),
modelName: modelName ?? this.modelName,
modelStatus: modelStatus ?? this.modelStatus,
@@ -554,6 +554,18 @@ class _ChatPageState extends State<ChatPage> {
: Icons.folder_outlined,
),
),
IconButton(
onPressed: () => context.read<ChatCubit>().toggleWebSearchMode(),
tooltip:
context.watch<ChatCubit>().state.isWebSearchMode
? 'إيقاف البحث العميق على الإنترنت'
: 'بحث عميق على الإنترنت من عدة مصادر',
color:
context.watch<ChatCubit>().state.isWebSearchMode
? _green
: _ink,
icon: const Icon(Icons.travel_explore_rounded),
),
if (context.watch<ChatCubit>().state.availableModels.isNotEmpty)
ConstrainedBox(
constraints: BoxConstraints(maxWidth: compact ? 100 : 190),
@@ -690,7 +702,9 @@ class _ChatPageState extends State<ChatPage> {
const SizedBox(height: 8),
Text(
context.watch<ChatCubit>().state.isWorkspaceMode
? 'وضع الوكيل يبحث في ملفات المشروع للقراءة فقط.'
? 'وضع الملفات: اسأل عن ملفات الكود في مساحة العمل للقراءة فقط.'
: context.watch<ChatCubit>().state.isWebSearchMode
? 'بحث عميق: يجلب مصادر ويب متعددة ويلخصها مع الروابط.'
: 'اكتب سؤالك، وسأرسله إلى النموذج المحلي على جهازك.',
style: const TextStyle(color: Color(0xFF718078), fontSize: 14),
),
@@ -947,7 +961,9 @@ class _ChatPageState extends State<ChatPage> {
decoration: InputDecoration(
hintText:
chat.isWorkspaceMode
? 'اسأل عن ملفات مساحة العمل…'
? 'اسأل عن ملف كود أو عن مساحة العمل…'
: chat.isWebSearchMode
? 'ما الموضوع الذي تريد البحث عنه على الإنترنت؟'
: 'اكتب رسالتك هنا…',
hintStyle: TextStyle(color: Color(0xFF9BA59E)),
border: InputBorder.none,