import 'dart:convert'; import '../http_util.dart'; import '../models.dart'; import '../sse.dart'; import 'webview_base_provider.dart'; /// HuggingChat (huggingface.co/chat). Guest REST is blocked; WebView carries cookies. class HuggingChat extends WebViewBaseProvider { @override String get name => 'HuggingChat'; @override bool get working => true; @override List get models => [ 'omni', 'meta-llama/Llama-3.1-8B-Instruct', 'Qwen/Qwen3.8-27B', 'zai-org/GLM-5.3-Flash', ]; @override String get defaultModel => 'omni'; @override String get baseUrl => 'https://huggingface.co/chat/'; @override String get apiEndpoint => 'https://huggingface.co/chat/api/v2/conversations'; @override String buildJsFetchCode(String model, List messages, bool stream) { final modelJson = jsonEncode(models.contains(model) ? model : defaultModel); final promptJson = jsonEncode(formatPrompt(messages)); return ''' (async function() { const headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}; let convRes = await fetch('/chat/api/v2/conversations', { method: 'POST', headers, body: JSON.stringify({ model: $modelJson }) }); if (!convRes.ok) { convRes = await fetch('/chat/conversation', { method: 'POST', headers, body: JSON.stringify({ model: $modelJson }) }); } if (!convRes.ok) { return JSON.stringify({error: 'HuggingChat login required: ' + convRes.status + ' ' + await convRes.text()}); } const text = await convRes.text(); const conv = text ? JSON.parse(text) : {}; const id = conv.json?.id || conv.id || conv.conversationId; if (!id) return JSON.stringify({error: 'no conversation id', raw: conv}); const payload = { id: crypto.randomUUID(), inputs: $promptJson, is_retry: false, is_continue: false, web_search: false, tools: [] }; let msgRes = await fetch('/chat/api/v2/conversations/' + id, { method: 'POST', headers: {'Content-Type': 'application/json', 'Accept': 'text/event-stream'}, body: JSON.stringify(payload) }); if (!msgRes.ok) { msgRes = await fetch('/chat/conversation/' + id, { method: 'POST', headers: {'Content-Type': 'application/json', 'Accept': 'text/event-stream'}, body: JSON.stringify(payload) }); } const body = await msgRes.text(); return JSON.stringify({status: msgRes.status, body}); })(); '''; } @override String parseResponse(String rawResponse) { try { final json = jsonDecode(rawResponse); if (json is Map && json['error'] != null) { throw Exception(json['error']); } final body = json is Map ? (json['body']?.toString() ?? rawResponse) : rawResponse; final sse = extractAllSseContent(body).join(); if (looksLikeModelOutput(sse)) return sse; // chat-ui tokens: {"type":"stream","token":"..."} final tokens = []; for (final line in body.split('\n')) { try { final m = jsonDecode(line); if (m is Map) { final token = m['token'] ?? m['text'] ?? m['generated_text']; if (token is String && token.isNotEmpty) tokens.add(token); } } catch (_) {} } final joined = tokens.join(); if (looksLikeModelOutput(joined)) return joined; if (looksLikeModelOutput(body)) return body.trim(); throw Exception('HuggingChat unusable: ${body.substring(0, body.length.clamp(0, 180))}'); } catch (e) { if (looksLikeModelOutput(rawResponse)) return rawResponse.trim(); throw Exception('Failed to parse HuggingChat: $e'); } } }