AUXteam commited on
Commit
56ad74b
·
verified ·
1 Parent(s): cf7a4c3

Add debug logging for API key

Browse files
Files changed (2) hide show
  1. app.py +8 -2
  2. check_upload/app.py +2580 -0
app.py CHANGED
@@ -821,11 +821,17 @@ async def generate_response(prompt: str, max_new_tokens: int = MAX_NEW_TOKENS) -
821
  selected_model = "alias-large" if prompt_tokens > 4000 else "alias-fast"
822
 
823
  url = f"{REMOTE_API_BASE}/completions"
 
 
 
 
 
 
824
  headers = {
825
  "Content-Type": "application/json"
826
  }
827
- if BLABLADOR_API_KEY:
828
- headers["Authorization"] = f"Bearer {BLABLADOR_API_KEY}"
829
  payload = {
830
  "model": selected_model,
831
  "prompt": prompt,
 
821
  selected_model = "alias-large" if prompt_tokens > 4000 else "alias-fast"
822
 
823
  url = f"{REMOTE_API_BASE}/completions"
824
+ print(f"Generating response with model: {selected_model}")
825
+ print(f"URL: {url}")
826
+ print(f"API Key present: {bool(BLABLADOR_API_KEY)}")
827
+ if BLABLADOR_API_KEY:
828
+ print(f"API Key length: {len(BLABLADOR_API_KEY)}")
829
+
830
  headers = {
831
  "Content-Type": "application/json"
832
  }
833
+ if BLABLADOR_API_KEY and BLABLADOR_API_KEY.strip():
834
+ headers["Authorization"] = f"Bearer {BLABLADOR_API_KEY.strip()}"
835
  payload = {
836
  "model": selected_model,
837
  "prompt": prompt,
check_upload/app.py ADDED
@@ -0,0 +1,2580 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ OpenResearcher DeepSearch Agent - Hugging Face Space
4
+ Uses ZeroGPU for efficient inference with the Nemotron model
5
+ Aligned with app_local.py frontend and logic
6
+ """
7
+ import os
8
+ import gradio as gr
9
+ import httpx
10
+ import json
11
+ import json5
12
+ import re
13
+ import time
14
+ import html
15
+ import asyncio
16
+ from datetime import datetime
17
+ from typing import List, Dict, Any, Optional, Tuple, Generator
18
+ import traceback
19
+ import base64
20
+ from transformers import AutoTokenizer
21
+ from gradio_client import Client as GradioClient
22
+
23
+
24
+
25
+ try:
26
+ from dotenv import load_dotenv
27
+ load_dotenv()
28
+ except ImportError:
29
+ pass
30
+
31
+ # ============================================================
32
+ # Configuration
33
+ # ============================================================
34
+ MODEL_NAME = os.getenv("MODEL_NAME", "alias-fast")
35
+ REMOTE_API_BASE = os.getenv("REMOTE_API_BASE", "https://api.helmholtz-blablador.fz-juelich.de/v1")
36
+ BLABLADOR_API_KEY = os.getenv("BLABLADOR_API_KEY", "")
37
+ MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "4096"))
38
+
39
+ # ============================================================
40
+ # System Prompt & Tools
41
+ # ============================================================
42
+ DEVELOPER_CONTENT = """
43
+ You are a helpful assistant and harmless assistant.
44
+
45
+ You will be able to use a set of browsering tools to answer user queries.
46
+
47
+ Tool for browsing.
48
+ The `cursor` appears in brackets before each browsing display: `[{cursor}]`.
49
+ Cite information from the tool using the following format:
50
+ `【{cursor}†L{line_start}(-L{line_end})?】`, for example: `【6†L9-L11】` or `【8†L3】`.
51
+ Do not quote more than 10 words directly from the tool output.
52
+ sources=web
53
+ """.strip()
54
+
55
+ TOOL_CONTENT = """
56
+ [
57
+ {
58
+ "type": "function",
59
+ "function": {
60
+ "name": "browser.search",
61
+ "description": "Searches for information related to a query and displays top N results. Returns a list of search results with titles, URLs, and summaries.",
62
+ "parameters": {
63
+ "type": "object",
64
+ "properties": {
65
+ "query": {
66
+ "type": "string",
67
+ "description": "The search query string"
68
+ },
69
+ "topn": {
70
+ "type": "integer",
71
+ "description": "Number of results to display",
72
+ "default": 10
73
+ }
74
+ },
75
+ "required": [
76
+ "query"
77
+ ]
78
+ }
79
+ }
80
+ },
81
+ {
82
+ "type": "function",
83
+ "function": {
84
+ "name": "browser.open",
85
+ "description": "Opens a link from the current page or a fully qualified URL. Can scroll to a specific location and display a specific number of lines. Valid link ids are displayed with the formatting: 【{id}†.*】.",
86
+ "parameters": {
87
+ "type": "object",
88
+ "properties": {
89
+ "id": {
90
+ "type": [
91
+ "integer",
92
+ "string"
93
+ ],
94
+ "description": "Link id from current page (integer) or fully qualified URL (string). Default is -1 (most recent page)",
95
+ "default": -1
96
+ },
97
+ "cursor": {
98
+ "type": "integer",
99
+ "description": "Page cursor to operate on. If not provided, the most recent page is implied",
100
+ "default": -1
101
+ },
102
+ "loc": {
103
+ "type": "integer",
104
+ "description": "Starting line number. If not provided, viewport will be positioned at the beginning or centered on relevant passage",
105
+ "default": -1
106
+ },
107
+ "num_lines": {
108
+ "type": "integer",
109
+ "description": "Number of lines to display",
110
+ "default": -1
111
+ },
112
+ "view_source": {
113
+ "type": "boolean",
114
+ "description": "Whether to view page source",
115
+ "default": false
116
+ },
117
+ "source": {
118
+ "type": "string",
119
+ "description": "The source identifier (e.g., 'web')"
120
+ }
121
+ },
122
+ "required": []
123
+ }
124
+ }
125
+ },
126
+ {
127
+ "type": "function",
128
+ "function": {
129
+ "name": "browser.find",
130
+ "description": "Finds exact matches of a pattern in the current page or a specified page by cursor.",
131
+ "parameters": {
132
+ "type": "object",
133
+ "properties": {
134
+ "pattern": {
135
+ "type": "string",
136
+ "description": "The exact text pattern to search for"
137
+ },
138
+ "cursor": {
139
+ "type": "integer",
140
+ "description": "Page cursor to search in. If not provided, searches in the current page",
141
+ "default": -1
142
+ }
143
+ },
144
+ "required": [
145
+ "pattern"
146
+ ]
147
+ }
148
+ }
149
+ }
150
+ ]
151
+ """.strip()
152
+
153
+ # ============================================================
154
+ # Browser Tool Implementation
155
+ # ============================================================
156
+ class SimpleBrowser:
157
+ """Browser tool using victor/websearch Gradio API."""
158
+
159
+ def __init__(self):
160
+ self.pages: Dict[str, Dict] = {}
161
+ self.page_stack: List[str] = []
162
+ self.link_map: Dict[int, Dict] = {} # Map from cursor ID (int) to {url, title}
163
+ self.used_citations = [] # List of cursor IDs (int) in order of first appearance
164
+ try:
165
+ # victor/websearch is a public space, but we can pass token if available
166
+ hf_token = os.getenv("HF_TOKEN", "")
167
+ # Use 'token' instead of 'hf_token' for Gradio Client
168
+ self.client = GradioClient("victor/websearch", token=hf_token if hf_token else None)
169
+ except Exception as e:
170
+ print(f"Error initializing Gradio client: {e}")
171
+ self.client = None
172
+
173
+ @property
174
+ def current_cursor(self) -> int:
175
+ return len(self.page_stack) - 1
176
+
177
+ def add_link(self, cursor: int, url: str, title: str = ""):
178
+ self.link_map[cursor] = {'url': url, 'title': title}
179
+
180
+ def get_link_info(self, cursor: int) -> Optional[dict]:
181
+ return self.link_map.get(cursor)
182
+
183
+ def get_citation_index(self, cursor: int) -> int:
184
+ if cursor not in self.used_citations:
185
+ self.used_citations.append(cursor)
186
+ return self.used_citations.index(cursor)
187
+
188
+ def get_page_info(self, cursor: int) -> Optional[Dict[str, str]]:
189
+ if cursor in self.link_map:
190
+ return self.link_map[cursor]
191
+ if 0 <= cursor < len(self.page_stack):
192
+ url = self.page_stack[cursor]
193
+ page = self.pages.get(url)
194
+ if page:
195
+ return {'url': url, 'title': page.get('title', '')}
196
+ return None
197
+
198
+ def _format_line_numbers(self, text: str, offset: int = 0) -> str:
199
+ lines = text.split('\n')
200
+ return '\n'.join(f"L{i + offset}: {line}" for i, line in enumerate(lines))
201
+
202
+ def _parse_websearch_output(self, output: str) -> List[Dict]:
203
+ results = []
204
+ # Split by the separator ---, handling potential variations in newlines
205
+ parts = re.split(r'\n---\n|^\s*---\s*$', output, flags=re.MULTILINE)
206
+ for part in parts:
207
+ part = part.strip()
208
+ if not part or "Successfully extracted content" in part:
209
+ continue
210
+
211
+ title_match = re.search(r'## (.*)', part)
212
+ domain_match = re.search(r'\*\*Domain:\*\* (.*)', part)
213
+ url_match = re.search(r'\*\*URL:\*\* (.*)', part)
214
+
215
+ if title_match and url_match:
216
+ title = title_match.group(1).strip()
217
+ url = url_match.group(1).strip()
218
+ domain = domain_match.group(1).strip() if domain_match else ""
219
+
220
+ # Content starts after metadata
221
+ metadata_end = url_match.end()
222
+ content = part[metadata_end:].strip()
223
+
224
+ results.append({
225
+ 'title': title,
226
+ 'url': url,
227
+ 'domain': domain,
228
+ 'content': content
229
+ })
230
+ return results
231
+
232
+ async def search(self, query: str, topn: int = 4) -> str:
233
+ if not self.client:
234
+ return "Error: Search client not initialized"
235
+
236
+ try:
237
+ # Call the Gradio API
238
+ loop = asyncio.get_event_loop()
239
+ result_str = await loop.run_in_executor(
240
+ None,
241
+ lambda: self.client.predict(
242
+ query=query,
243
+ search_type="search",
244
+ num_results=topn,
245
+ api_name="/search_web"
246
+ )
247
+ )
248
+
249
+ results = self._parse_websearch_output(result_str)
250
+ if not results:
251
+ return f"No results found for: '{query}'"
252
+
253
+ # Populate pages and link_map
254
+ new_link_map = {}
255
+ lines = []
256
+
257
+ for i, r in enumerate(results):
258
+ title = r['title']
259
+ url = r['url']
260
+ domain = r['domain']
261
+ content = r['content']
262
+
263
+ # Create a snippet for the search result view
264
+ snippet = content[:200].replace('\n', ' ') + "..."
265
+
266
+ self.link_map[i] = {'url': url, 'title': title}
267
+ new_link_map[i] = {'url': url, 'title': title}
268
+
269
+ # Cache the full content
270
+ self.pages[url] = {
271
+ 'url': url,
272
+ 'title': title,
273
+ 'text': content
274
+ }
275
+
276
+ link_text = f"【{i}†{title}†{domain}】" if domain else f"【{i}†{title}】"
277
+ lines.append(f"{link_text}")
278
+ lines.append(f" {snippet}")
279
+ lines.append("")
280
+
281
+ formatted_content = '\n'.join(lines)
282
+ pseudo_url = f"web-search://q={query}&ts={int(time.time())}"
283
+ cursor = self.current_cursor + 1
284
+
285
+ self.pages[pseudo_url] = {
286
+ 'url': pseudo_url,
287
+ 'title': f"Search Results: {query}",
288
+ 'text': formatted_content,
289
+ 'urls': {str(k): v['url'] for k, v in new_link_map.items()}
290
+ }
291
+ self.page_stack.append(pseudo_url)
292
+
293
+ header = f"Search Results: {query} ({pseudo_url})\n**viewing lines [0 - {len(formatted_content.split(chr(10)))-1}]**\n\n"
294
+ body = self._format_line_numbers(formatted_content)
295
+
296
+ return f"[{cursor}] {header}{body}"
297
+
298
+ except Exception as e:
299
+ return f"Error during search: {str(e)}"
300
+
301
+ async def open(self, id: int | str = -1, cursor: int = -1, loc: int = -1, num_lines: int = -1, **kwargs) -> str:
302
+ target_url = None
303
+
304
+ if isinstance(id, str) and id.startswith("http"):
305
+ target_url = id
306
+ elif isinstance(id, int) and id >= 0:
307
+ info = self.link_map.get(id)
308
+ target_url = info['url'] if info else None
309
+ if not target_url:
310
+ return f"Error: Invalid link id '{id}'. Available: {list(self.link_map.keys())}"
311
+ elif cursor >= 0 and cursor < len(self.page_stack):
312
+ page_url = self.page_stack[cursor]
313
+ page = self.pages.get(page_url)
314
+ if page:
315
+ text = page['text']
316
+ lines = text.split('\n')
317
+ start = max(0, loc) if loc >= 0 else 0
318
+ end = min(len(lines), start + num_lines) if num_lines > 0 else len(lines)
319
+
320
+ header = f"{page['title']} ({page['url']})\n**viewing lines [{start} - {end-1}] of {len(lines)-1}**\n\n"
321
+ body = self._format_line_numbers('\n'.join(lines[start:end]), offset=start)
322
+ return f"[{cursor}] {header}{body}"
323
+ else:
324
+ return "Error: No valid target specified"
325
+
326
+ if not target_url:
327
+ return "Error: Could not determine target URL"
328
+
329
+ # Check if we already have the page content cached
330
+ if target_url in self.pages:
331
+ page = self.pages[target_url]
332
+ text = page['text']
333
+ lines = text.split('\n')
334
+
335
+ new_cursor = self.current_cursor + 1
336
+ self.page_stack.append(target_url)
337
+
338
+ start = max(0, loc) if loc >= 0 else 0
339
+ end = min(len(lines), start + num_lines) if num_lines > 0 else len(lines)
340
+
341
+ header = f"{page['title']} ({target_url})\n**viewing lines [{start} - {end-1}] of {len(lines)-1}**\n\n"
342
+ body = self._format_line_numbers('\n'.join(lines[start:end]), offset=start)
343
+ return f"[{new_cursor}] {header}{body}"
344
+
345
+ return f"Error: Content for {target_url} not found in search results. The current search API only provides content for pages returned in search results."
346
+
347
+ def find(self, pattern: str, cursor: int = -1) -> str:
348
+ if not self.page_stack:
349
+ return "Error: No page open"
350
+
351
+ page_url = self.page_stack[cursor] if cursor >= 0 and cursor < len(self.page_stack) else self.page_stack[-1]
352
+ page = self.pages.get(page_url)
353
+
354
+ if not page:
355
+ return "Error: Page not found"
356
+
357
+ text = page['text']
358
+ lines = text.split('\n')
359
+ matches = []
360
+
361
+ for i, line in enumerate(lines):
362
+ if str(pattern).lower() in line.lower():
363
+ start = max(0, i - 1)
364
+ end = min(len(lines), i + 3)
365
+ context = '\n'.join(f"L{j}: {lines[j]}" for j in range(start, end))
366
+ matches.append(f"# 【{len(matches)}†match at L{i}】\n{context}")
367
+ if len(matches) >= 10:
368
+ break
369
+
370
+ if not matches:
371
+ return f"No matches found for: '{pattern}'"
372
+
373
+ result_url = f"{page_url}/find?pattern={pattern}"
374
+ new_cursor = self.current_cursor + 1
375
+ result_content = '\n\n'.join(matches)
376
+
377
+ page_data = {
378
+ 'url': result_url,
379
+ 'title': f"Find results for: '{pattern}'",
380
+ 'text': result_content,
381
+ 'urls': {}
382
+ }
383
+ self.pages[result_url] = page_data
384
+ self.page_stack.append(result_url)
385
+
386
+ header = f"Find results for text: `{pattern}` in `{page['title']}`\n\n"
387
+ return f"[{new_cursor}] {header}{result_content}"
388
+
389
+ def get_cursor_url(self, cursor: int) -> Optional[str]:
390
+ if cursor >= 0 and cursor < len(self.page_stack):
391
+ return self.page_stack[cursor]
392
+ return None
393
+
394
+
395
+ # ============================================================
396
+ # Tokenizer Loading
397
+ # ============================================================
398
+ tokenizer = None
399
+
400
+ def load_tokenizer():
401
+ global tokenizer
402
+ if tokenizer is None:
403
+ # We use Nemotron as a proxy tokenizer for token counting
404
+ token_model = "OpenResearcher/Nemotron-3-Nano-30B-A3B"
405
+ print(f"Loading tokenizer: {token_model}")
406
+ try:
407
+ tokenizer = AutoTokenizer.from_pretrained(
408
+ token_model,
409
+ trust_remote_code=True
410
+ )
411
+ print("Tokenizer loaded successfully!")
412
+ except Exception as e:
413
+ print(f"Error loading tokenizer: {e}")
414
+ import traceback
415
+ traceback.print_exc()
416
+ raise
417
+ return tokenizer
418
+
419
+ # ============================================================
420
+ # Text Processing
421
+ # ============================================================
422
+ def extract_thinking(text: str) -> Tuple[Optional[str], str]:
423
+ reasoning_content = None
424
+ content = text
425
+
426
+ if '<think>' in content and '</think>' in content:
427
+ match = re.search(r'<think>(.*?)</think>', content, re.DOTALL)
428
+ if match:
429
+ reasoning_content = match.group(1).strip()
430
+ content = content.replace(match.group(0), "").strip()
431
+ elif '</think>' in content:
432
+ match = re.search(r'^(.*?)</think>', content, re.DOTALL)
433
+ if match:
434
+ reasoning_content = match.group(1).strip()
435
+ content = content.replace(match.group(0), "").strip()
436
+
437
+ return reasoning_content, content
438
+
439
+
440
+ def parse_tool_call(text: str) -> Tuple[Optional[Dict], str]:
441
+ tool_call_text = None
442
+ content = text
443
+
444
+ if '<tool_call>' in content and '</tool_call>' in content:
445
+ match = re.search(r'<tool_call>(.*?)</tool_call>', content, re.DOTALL)
446
+ if match:
447
+ tool_call_text = match.group(1).strip()
448
+ content = content.replace(match.group(0), "").strip()
449
+ elif '</tool_call>' in content:
450
+ match = re.search(r'^(.*?)</tool_call>', content, re.DOTALL)
451
+ if match:
452
+ tool_call_text = match.group(1).strip()
453
+ content = content.replace(match.group(0), "").strip()
454
+
455
+ if tool_call_text:
456
+ try:
457
+ if "```json" in tool_call_text:
458
+ tool_call_text = tool_call_text.split("```json")[1].split("```")[0].strip()
459
+ elif "```" in tool_call_text:
460
+ tool_call_text = tool_call_text.split("```")[1].split("```")[0].strip()
461
+
462
+ parsed = json5.loads(tool_call_text)
463
+ return parsed, content
464
+ except:
465
+ pass
466
+
467
+ func_match = re.search(r'<function=([\w.]+)>', tool_call_text)
468
+ if func_match:
469
+ tool_name = func_match.group(1)
470
+ tool_args = {}
471
+ params = re.finditer(r'<parameter=([\w]+)>\s*(.*?)\s*</parameter>', tool_call_text, re.DOTALL)
472
+ for p in params:
473
+ param_name = p.group(1)
474
+ param_value = p.group(2).strip()
475
+ if param_value.startswith('"') and param_value.endswith('"'):
476
+ param_value = param_value[1:-1]
477
+ try:
478
+ if param_value.isdigit():
479
+ param_value = int(param_value)
480
+ except:
481
+ pass
482
+ tool_args[param_name] = param_value
483
+
484
+ return {"name": tool_name, "arguments": tool_args}, content
485
+
486
+ return None, content
487
+
488
+
489
+ def is_final_answer(text: str) -> bool:
490
+ t = text.lower()
491
+ return (
492
+ ('<answer>' in t and '</answer>' in t) or
493
+ 'final answer:' in t or
494
+ ('exact answer:' in t and 'confidence:' in t)
495
+ )
496
+
497
+ # ============================================================
498
+ # HTML Rendering Helpers (From app_local.py)
499
+ # ============================================================
500
+ def render_citations(text: str, browser: SimpleBrowser) -> str:
501
+ """Convert citation markers to clickable HTML links."""
502
+ def replace_citation(m):
503
+ cursor_str = m.group(1)
504
+ # l1 = m.group(2)
505
+ # l2 = m.group(3)
506
+
507
+ try:
508
+ cursor = int(cursor_str)
509
+ index = browser.get_citation_index(cursor)
510
+
511
+ # Check if we have URL info
512
+ info = browser.get_page_info(cursor)
513
+ if info and info.get('url'):
514
+ # Return clickable index link pointing to reference section
515
+ # Aligned with generate_html_example.py style (green via CSS class)
516
+ url = info.get('url')
517
+ return f'<a href="{html.escape(url)}" target="_blank" class="citation-link">[{index}]</a>'
518
+
519
+ # Fallback if no URL
520
+ return f'<span class="citation-link">[{index}]</span>'
521
+ except Exception as e:
522
+ # print(f"Error in replace_citation: {e}, match: {m.group(0)}")
523
+ pass
524
+ return m.group(0)
525
+
526
+ # First pass: replace citations with linked citations
527
+ result = re.sub(r'[【\[](\d+)†.*?[】\]]', replace_citation, text)
528
+
529
+ # Second pass: Deduplicate adjacent identical citations
530
+ # Matches: <a ...>[N]</a> followed by optional whitespace and same link
531
+ # We repeat this until no more changes to handle multiple duplicates
532
+ while True:
533
+ new_result = re.sub(r'(<a [^>]+>\[\d+\]</a>)(\s*)\1', r'\1', result)
534
+ if new_result == result:
535
+ break
536
+ result = new_result
537
+
538
+ # Convert basic markdown to HTML
539
+ result = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', result)
540
+ result = re.sub(r'\*(.+?)\*', r'<em>\1</em>', result)
541
+ result = re.sub(r'`(.+?)`', r'<code>\1</code>', result)
542
+ result = result.replace('\n\n', '</p><p>').replace('\n', '<br>')
543
+ if not result.startswith('<p>'):
544
+ result = f'<p>{result}</p>'
545
+
546
+ return result
547
+
548
+ def render_thinking_streaming(text: str) -> str:
549
+ """Render thinking content in streaming mode (visible, with animation)."""
550
+ escaped = html.escape(text)
551
+ return f'<div class="thinking-streaming">{escaped}</div>'
552
+
553
+ def render_thinking_collapsed(text: str) -> str:
554
+ """Render thinking content in collapsed mode after completion."""
555
+ escaped = html.escape(text)
556
+ preview = text[:100] + "..." if len(text) > 100 else text
557
+ preview_escaped = html.escape(preview)
558
+ return f'''<details class="thinking-collapsed">
559
+ <summary>Thought process: "{preview_escaped}"</summary>
560
+ <div class="thinking-content">{escaped}</div>
561
+ </details>'''
562
+
563
+ def render_tool_call(fn_name: str, args: dict, browser: SimpleBrowser = None) -> str:
564
+ """Render a tool call card with unified format and subtle distinction."""
565
+ border_colors = {
566
+ "browser.search": "#667eea",
567
+ "browser.open": "#4facfe",
568
+ "browser.find": "#fa709a"
569
+ }
570
+ border_color = border_colors.get(fn_name, "#9ca3af")
571
+
572
+ if fn_name == "browser.search":
573
+ query = str(args.get('query', ''))
574
+ return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
575
+ <div class="tool-info">
576
+ <div class="tool-name">Searching the web</div>
577
+ <div class="tool-detail">Query: "{html.escape(query)}"</div>
578
+ </div>
579
+ </div>'''
580
+ elif fn_name == "browser.open":
581
+ link_id = args.get('id', '')
582
+ url_info = ""
583
+ if browser and isinstance(link_id, int) and link_id >= 0:
584
+ info = browser.link_map.get(link_id)
585
+ url = info.get('url', "") if info else ""
586
+ if url:
587
+ try:
588
+ domain = url.split('/')[2]
589
+ url_info = f" → {domain}"
590
+ except:
591
+ url_info = ""
592
+ return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
593
+ <div class="tool-info">
594
+ <div class="tool-name">Opening page</div>
595
+ <div class="tool-detail">Link #{link_id}{url_info}</div>
596
+ </div>
597
+ </div>'''
598
+ elif fn_name == "browser.find":
599
+ pattern = str(args.get('pattern', ''))
600
+ return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
601
+ <div class="tool-info">
602
+ <div class="tool-name">Finding in page</div>
603
+ <div class="tool-detail">Pattern: "{html.escape(pattern)}"</div>
604
+ </div>
605
+ </div>'''
606
+ else:
607
+ return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
608
+ <div class="tool-info">
609
+ <div class="tool-name">{html.escape(str(fn_name))}</div>
610
+ <div class="tool-detail">{html.escape(json.dumps(args))}</div>
611
+ </div>
612
+ </div>'''
613
+
614
+ def render_tool_result(result: str, fn_name: str) -> str:
615
+ """Render tool result in an expanded card with direct HTML rendering."""
616
+ import uuid
617
+ tool_label = {
618
+ "browser.search": "🔍 Search Results",
619
+ "browser.open": "📄 Page Content",
620
+ "browser.find": "🔎 Find Results"
621
+ }.get(fn_name, "📋 Result")
622
+
623
+ border_colors = {
624
+ "browser.search": "#667eea",
625
+ "browser.open": "#4facfe",
626
+ "browser.find": "#86efac"
627
+ }
628
+ border_color = border_colors.get(fn_name, "#9ca3af")
629
+
630
+ # ===== SEARCH RESULTS =====
631
+ if fn_name == "browser.search" and "<html>" in result and "<ul>" in result:
632
+ ul_match = re.search(r'<ul>(.*?)</ul>', result, re.DOTALL)
633
+ if ul_match:
634
+ ul_content = ul_match.group(1)
635
+ items = re.findall(r"<li><a href='([^']+)'>([^<]+)</a>\s*([^<]*)</li>", ul_content)
636
+
637
+ if items:
638
+ lines = result.split('\n')
639
+ search_title = ""
640
+ if lines and re.match(r'^\[\d+\]\s+Search Results:', lines[0]):
641
+ match = re.match(r'^\[(\d+)\]\s+(.+?)\s+\(web-search://', lines[0])
642
+ if match:
643
+ ref_num, title = match.groups()
644
+ title = re.sub(r'\s+\(web-search://.*$', '', lines[0])
645
+ title = re.sub(r'^\[\d+\]\s+', '', title)
646
+ search_title = f'''
647
+ <div style="background: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%); padding: 0.875rem 1rem; margin: -1.25rem -1.25rem 1rem -1.25rem; border-bottom: 1px solid #e0e7ff;">
648
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
649
+ <span style="background: #667eea; color: white; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600;">【{ref_num}】</span>
650
+ <span style="color: #1e40af; font-weight: 600; font-size: 0.95rem;">{html.escape(title)}</span>
651
+ </div>
652
+ </div>
653
+ '''
654
+
655
+ result_html = '<div style="display: flex; flex-direction: column; gap: 0.75rem;">'
656
+ for idx, (url, title, summary) in enumerate(items, 1):
657
+ card_id = f"search-card-{uuid.uuid4().hex[:8]}"
658
+ result_html += f'''
659
+ <div class="search-result-card" style="background: white; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; transition: all 0.2s ease;">
660
+ <div style="display: flex; align-items: center; gap: 0.5rem; padding: 0.875rem;">
661
+ <a href="{html.escape(url)}" target="_blank"
662
+ style="color: #667eea; font-weight: 600; font-size: 0.75rem; min-width: 30px; text-decoration: none;">【{idx}】</a>
663
+ <a href="{html.escape(url)}" target="_blank"
664
+ style="color: #1f2937; font-weight: 600; font-size: 0.9rem; text-decoration: none; flex: 1;">
665
+ {html.escape(title)}
666
+ </a>
667
+ </div>
668
+ <div style="padding: 0 0.875rem 0.875rem 0.875rem; border-top: 1px solid #f3f4f6;">
669
+ <div style="color: #6b7280; font-size: 0.85rem; line-height: 1.5; margin-top: 0.75rem;">
670
+ {html.escape(summary)}
671
+ </div>
672
+ <div style="color: #9ca3af; font-size: 0.75rem; margin-top: 0.5rem; font-family: monospace; word-break: break-all;">
673
+ {html.escape(url)}
674
+ </div>
675
+ </div>
676
+ </div>
677
+ '''
678
+ result_html += '</div>'
679
+ return f'''<div class="result-card-expanded" style="border-left: 3px solid {border_color};">
680
+ <div class="result-header-expanded">{tool_label}</div>
681
+ <div class="result-content-expanded" style="font-family: inherit;">{search_title}{result_html}</div>
682
+ </div>'''
683
+
684
+ # ===== BROWSER.OPEN and BROWSER.FIND =====
685
+ lines = result.split('\n')
686
+ title_html = ""
687
+ content_start_idx = 0
688
+ pattern_to_highlight = None
689
+
690
+ if lines and re.match(r'^\[\d+\]\s+.+\s+\(.+\)$', lines[0]):
691
+ first_line = lines[0]
692
+ match = re.match(r'^\[(\d+)\]\s+(.+?)\s+\((.+)\)$', first_line)
693
+ if match:
694
+ ref_num, title, url = match.groups()
695
+
696
+ if fn_name == "browser.find":
697
+ pattern_match = re.search(r'Find Results:\s*(.+)', title)
698
+ if pattern_match:
699
+ pattern_to_highlight = pattern_match.group(1).strip()
700
+
701
+ is_clickable = not url.startswith('web-search://')
702
+
703
+ if is_clickable:
704
+ title_html = f'''
705
+ <div style="background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); padding: 0.875rem 1rem; margin: -1.25rem -1.25rem 1rem -1.25rem; border-bottom: 1px solid #e0e7ff;">
706
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
707
+ <span style="background: {border_color}; color: white; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600;">【{ref_num}】</span>
708
+ <a href="{html.escape(url)}" target="_blank"
709
+ style="color: #1e40af; font-weight: 600; font-size: 0.95rem; text-decoration: none; flex: 1;">
710
+ {html.escape(title)}
711
+ </a>
712
+ </div>
713
+ <div style="color: #64748b; font-size: 0.75rem; margin-top: 0.25rem; font-family: monospace;">
714
+ {html.escape(url)}
715
+ </div>
716
+ </div>
717
+ '''
718
+ else:
719
+ title_html = f'''
720
+ <div style="background: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%); padding: 0.875rem 1rem; margin: -1.25rem -1.25rem 1rem -1.25rem; border-bottom: 1px solid #e0e7ff;">
721
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
722
+ <span style="background: {border_color}; color: white; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600;">【{ref_num}】</span>
723
+ <span style="color: #1e40af; font-weight: 600; font-size: 0.95rem;">{html.escape(title)}</span>
724
+ </div>
725
+ </div>
726
+ '''
727
+
728
+ content_start_idx = 1
729
+ if content_start_idx < len(lines) and lines[content_start_idx].startswith('**viewing lines'):
730
+ content_start_idx += 1
731
+ if content_start_idx < len(lines) and lines[content_start_idx].strip() == '':
732
+ content_start_idx += 1
733
+
734
+ cleaned_lines = []
735
+ for line in lines[content_start_idx:]:
736
+ cleaned_line = re.sub(r'^L\d+:\s*', '', line)
737
+ cleaned_lines.append(cleaned_line)
738
+
739
+ cleaned_content = '\n'.join(cleaned_lines)
740
+ formatted_result = html.escape(cleaned_content)
741
+
742
+ if pattern_to_highlight and fn_name == "browser.find":
743
+ escaped_pattern = re.escape(pattern_to_highlight)
744
+ def highlight_match(match):
745
+ return f'<mark style="background: #86efac; padding: 0.125rem 0.25rem; border-radius: 2px; font-weight: 600; color: #064e3b;">{match.group(0)}</mark>'
746
+ formatted_result = re.sub(
747
+ escaped_pattern,
748
+ highlight_match,
749
+ formatted_result,
750
+ flags=re.IGNORECASE
751
+ )
752
+
753
+ def make_citation_clickable(match):
754
+ full_text = match.group(0)
755
+ parts_match = re.match(r'【(\d+)†([^†]+)†([^】]+)】', full_text)
756
+ if parts_match:
757
+ ref_num = parts_match.group(1)
758
+ title = parts_match.group(2)
759
+ domain = parts_match.group(3)
760
+ url = f"https://{domain}" if not domain.startswith('http') else domain
761
+ return f'<a href="{html.escape(url)}" target="_blank" style="background: #e0f7fa; padding: 2px 6px; border-radius: 4px; font-size: 0.85em; color: #006064; font-weight: 500; text-decoration: none; display: inline-block;" title="{html.escape(title)}">【{ref_num}†{html.escape(domain)}】</a>'
762
+ else:
763
+ simple_match = re.match(r'【(\d+)†([^】]+)】', full_text)
764
+ if simple_match:
765
+ ref_num = simple_match.group(1)
766
+ text = simple_match.group(2)
767
+ return f'<span style="background: #e0f7fa; padding: 2px 6px; border-radius: 4px; font-size: 0.85em; color: #006064; font-weight: 500;">【{ref_num}†{html.escape(text)}】</span>'
768
+ return full_text
769
+
770
+ formatted_result = re.sub(r'【\d+†[^】]+】', make_citation_clickable, formatted_result)
771
+ formatted_result = formatted_result.replace('\n\n', '</p><p style="margin: 0.75rem 0;">')
772
+ formatted_result = formatted_result.replace('\n', '<br>')
773
+
774
+ if not formatted_result.startswith('<p'):
775
+ formatted_result = f'<p style="margin: 0.75rem 0;">{formatted_result}</p>'
776
+
777
+ max_length = 5000
778
+ if len(result) > max_length:
779
+ formatted_result = formatted_result[:max_length] + '<br><br><em style="color: #9ca3af;">...(content truncated for display)...</em>'
780
+
781
+ return f'''<div class="result-card-expanded" style="border-left: 3_solid {border_color};">
782
+ <div class="result-header-expanded">{tool_label}</div>
783
+ <div class="result-content-expanded" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; line-height: 1.7; color: #374151;">{title_html}{formatted_result}</div>
784
+ </div>'''
785
+
786
+ def render_round_badge(round_num: int, max_rounds: int) -> str:
787
+ return f'<div class="round-badge">Round {round_num}/{max_rounds}</div>'
788
+
789
+ def render_answer(text: str, browser: SimpleBrowser) -> str:
790
+ rendered = render_citations(text, browser)
791
+ return f'<div class="answer-section">{rendered}</div>'
792
+
793
+ def render_completion() -> str:
794
+ return '<div class="completion-msg">Research Complete</div>'
795
+
796
+ def render_user_message(question: str) -> str:
797
+ escaped = html.escape(question)
798
+ return f'''<div class="user-message-bubble">
799
+ <div class="user-message-content">{escaped}</div>
800
+ </div>'''
801
+
802
+
803
+
804
+ # ============================================================
805
+ # Remote API Generation (via OpenAI-compatible endpoint)
806
+ # ============================================================
807
+
808
+ def count_tokens(text: str) -> int:
809
+ """Count tokens in text using the loaded tokenizer."""
810
+ try:
811
+ tok = load_tokenizer()
812
+ return len(tok.encode(text))
813
+ except Exception:
814
+ # Fallback to rough estimate if tokenizer fails
815
+ return len(text) // 4
816
+
817
+ async def generate_response(prompt: str, max_new_tokens: int = MAX_NEW_TOKENS) -> str:
818
+ """Generate response using OpenAI-compatible API with model switching."""
819
+ # Choose model based on prompt length
820
+ prompt_tokens = count_tokens(prompt)
821
+ selected_model = "alias-large" if prompt_tokens > 4000 else "alias-fast"
822
+
823
+ url = f"{REMOTE_API_BASE}/completions"
824
+ headers = {
825
+ "Content-Type": "application/json"
826
+ }
827
+ if BLABLADOR_API_KEY:
828
+ headers["Authorization"] = f"Bearer {BLABLADOR_API_KEY}"
829
+ payload = {
830
+ "model": selected_model,
831
+ "prompt": prompt,
832
+ "max_tokens": max_new_tokens,
833
+ "temperature": 0.7,
834
+ "top_p": 0.9,
835
+ "stop": ["\n<tool_response>", "<tool_response>"],
836
+ }
837
+
838
+ async with httpx.AsyncClient() as client:
839
+ response = await client.post(url, json=payload, headers=headers, timeout=300.0)
840
+
841
+ if response.status_code != 200:
842
+ raise Exception(f"LLM API error {response.status_code}: {response.text}")
843
+
844
+ data = response.json()
845
+ return data["choices"][0]["text"]
846
+
847
+
848
+ # ============================================================
849
+ # Streaming Agent Runner
850
+ # ============================================================
851
+ async def run_agent_streaming(
852
+ question: str,
853
+ max_rounds: int
854
+ ) -> Generator[str, None, None]:
855
+ global tokenizer
856
+
857
+ if not question.strip():
858
+ yield "<p style='color: var(--body-text-color-subdued); text-align: center; padding: 2rem;'>Please enter a question to begin.</p>"
859
+ return
860
+
861
+ if not BLABLADOR_API_KEY:
862
+ yield """<div class="error-message">
863
+ <p><strong>BLABLADOR_API_KEY Missing</strong></p>
864
+ <p>The Blablador API Key is not configured in the Space secrets. Please add it to your Space settings.</p>
865
+ </div>"""
866
+ return
867
+
868
+ # Load tokenizer for prompt formatting
869
+ try:
870
+ load_tokenizer()
871
+ except Exception as e:
872
+ yield f"<p style='color:#dc2626;'>Error loading tokenizer: {html.escape(str(e))}</p>"
873
+ return
874
+
875
+ browser = SimpleBrowser()
876
+ tools = json.loads(TOOL_CONTENT)
877
+
878
+ system_prompt = DEVELOPER_CONTENT + f"\n\nToday's date: {datetime.now().strftime('%Y-%m-%d')}"
879
+ messages = [
880
+ {"role": "system", "content": system_prompt},
881
+ {"role": "user", "content": question}
882
+ ]
883
+
884
+ stop_strings = ["\n<tool_response>", "<tool_response>"]
885
+
886
+ html_parts = [render_user_message(question)]
887
+ yield ''.join(html_parts)
888
+
889
+ round_num = 0
890
+
891
+ try:
892
+ while round_num < max_rounds:
893
+ round_num += 1
894
+ html_parts.append(render_round_badge(round_num, max_rounds))
895
+ yield ''.join(html_parts)
896
+
897
+ prompt = tokenizer.apply_chat_template(
898
+ messages,
899
+ tools=tools,
900
+ tokenize=False,
901
+ add_generation_prompt=True
902
+ )
903
+
904
+ try:
905
+ print(f"\n{'='*60}")
906
+ print(f"Round {round_num}")
907
+ print(f"{'='*60}")
908
+
909
+ html_parts.append('<div class="thinking-streaming">Processing...</div>')
910
+ yield ''.join(html_parts)
911
+
912
+ # Call generation function
913
+ generated = await generate_response(prompt, max_new_tokens=MAX_NEW_TOKENS)
914
+
915
+ # Remove placeholder
916
+ html_parts.pop()
917
+
918
+ except Exception as e:
919
+ html_parts.pop() # Remove placeholder
920
+ html_parts.append(f"<p style='color:#dc2626;'>Generation Error: {html.escape(str(e))}</p>")
921
+ yield ''.join(html_parts)
922
+ return
923
+
924
+ for stop_str in stop_strings:
925
+ if stop_str in generated:
926
+ generated = generated[:generated.find(stop_str)]
927
+
928
+ reasoning, content = extract_thinking(generated)
929
+ tool_call, clean_content = parse_tool_call(content)
930
+
931
+ if reasoning:
932
+ html_parts.append(render_thinking_collapsed(reasoning))
933
+ yield ''.join(html_parts)
934
+
935
+ if tool_call:
936
+ fn_name = tool_call.get("name", "unknown")
937
+ args = tool_call.get("arguments", {})
938
+ html_parts.append(render_tool_call(fn_name, args, browser))
939
+ yield ''.join(html_parts)
940
+
941
+ if clean_content.strip() and not tool_call:
942
+ rendered = render_citations(clean_content, browser)
943
+ html_parts.append(f'<div class="answer-section">{rendered}</div>')
944
+ yield ''.join(html_parts)
945
+
946
+ non_thinking = generated.split('</think>', 1)[1].strip() if '</think>' in generated else generated.strip()
947
+ messages.append({
948
+ "role": "assistant",
949
+ "content": non_thinking if tool_call is None else "",
950
+ "reasoning_content": reasoning,
951
+ "tool_calls": [{
952
+ "id": str(round_num),
953
+ "type": "function",
954
+ "function": {
955
+ "name": tool_call.get("name", ""),
956
+ "arguments": tool_call.get("arguments", {})
957
+ }
958
+ }] if tool_call else None
959
+ })
960
+
961
+ if tool_call:
962
+ fn_name = tool_call.get("name", "")
963
+ args = tool_call.get("arguments", {})
964
+
965
+ if fn_name.startswith("browser."):
966
+ actual_fn = fn_name.split(".", 1)[1]
967
+ else:
968
+ actual_fn = fn_name
969
+
970
+ result = ""
971
+ try:
972
+ if actual_fn == "search":
973
+ result = await browser.search(args.get("query", ""), args.get("topn", 4))
974
+ elif actual_fn == "open":
975
+ result = await browser.open(**args)
976
+ elif actual_fn == "find":
977
+ result = browser.find(args.get("pattern", ""), args.get("cursor", -1))
978
+ else:
979
+ result = f"Unknown tool: {fn_name}"
980
+ except Exception as e:
981
+ result = f"Tool error: {str(e)}\n{traceback.format_exc()}"
982
+
983
+ html_parts.append(render_tool_result(result, fn_name))
984
+ yield ''.join(html_parts)
985
+
986
+ messages.append({
987
+ "role": "tool",
988
+ "tool_call_id": str(round_num),
989
+ "content": result
990
+ })
991
+ continue
992
+
993
+ if is_final_answer(generated):
994
+ html_parts.append(render_completion())
995
+ yield ''.join(html_parts)
996
+ break
997
+
998
+ if round_num >= max_rounds:
999
+ html_parts.append('<div class="completion-msg" style="background:#fef3c7;border-color:#f59e0b;color:#92400e;">Maximum rounds reached</div>')
1000
+ yield ''.join(html_parts)
1001
+
1002
+ # Generate Reference Section
1003
+ if browser.used_citations:
1004
+ html_parts.append('<details class="reference-section">')
1005
+ html_parts.append('<summary class="reference-title">References</summary>')
1006
+
1007
+ for i, cursor in enumerate(browser.used_citations):
1008
+ info = browser.get_page_info(cursor)
1009
+ if info:
1010
+ url = info.get('url', '#')
1011
+ title = info.get('title', 'Unknown Source')
1012
+ else:
1013
+ url = "#"
1014
+ title = "Unknown Source"
1015
+
1016
+ ref_item = f'''
1017
+ <div class="reference-item">
1018
+ <div style="display: flex; align-items: baseline;">
1019
+ <span class="ref-number">[{i}]</span>
1020
+ <a href="{html.escape(url)}" target="_blank" class="ref-text">{html.escape(title)}</a>
1021
+ </div>
1022
+ <div class="ref-url" style="text-align: left;">{html.escape(url)}</div>
1023
+ </div>
1024
+ '''
1025
+ html_parts.append(ref_item)
1026
+
1027
+ html_parts.append('</details>')
1028
+ yield ''.join(html_parts)
1029
+
1030
+ except Exception as e:
1031
+ tb = traceback.format_exc()
1032
+ html_parts.append(f'<div style="color:#dc2626;"><p>Error: {html.escape(str(e))}</p><pre>{html.escape(tb)}</pre></div>')
1033
+ yield ''.join(html_parts)
1034
+
1035
+
1036
+ # ============================================================
1037
+ # Gradio Interface
1038
+ # ============================================================
1039
+ CAROUSEL_JS = r"""
1040
+ (function() {
1041
+ let currentExample = 0;
1042
+ const totalExamples = 3;
1043
+ let carouselInitialized = false;
1044
+ let layoutInitialized = false;
1045
+
1046
+ function updateCarousel() {
1047
+ const items = document.querySelectorAll('.carousel-item');
1048
+ const dots = document.querySelectorAll('.carousel-dot');
1049
+
1050
+ items.forEach((item, index) => {
1051
+ if (index === currentExample) {
1052
+ item.classList.add('active');
1053
+ } else {
1054
+ item.classList.remove('active');
1055
+ }
1056
+ });
1057
+
1058
+ dots.forEach((dot, index) => {
1059
+ if (index === currentExample) {
1060
+ dot.classList.add('active');
1061
+ } else {
1062
+ dot.classList.remove('active');
1063
+ }
1064
+ });
1065
+ }
1066
+
1067
+ function setExample(text) {
1068
+ const container = document.querySelector('#question-input');
1069
+ if (container) {
1070
+ const textbox = container.querySelector('textarea');
1071
+ if (textbox) {
1072
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
1073
+ nativeInputValueSetter.call(textbox, text);
1074
+ textbox.dispatchEvent(new Event('input', { bubbles: true }));
1075
+ textbox.focus();
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ function initCarousel() {
1081
+ if (carouselInitialized) return;
1082
+
1083
+ const prevBtn = document.getElementById('prev-btn');
1084
+ const nextBtn = document.getElementById('next-btn');
1085
+ const items = document.querySelectorAll('.carousel-item');
1086
+ const dots = document.querySelectorAll('.carousel-dot');
1087
+
1088
+ if (!prevBtn || !nextBtn || items.length === 0) return;
1089
+
1090
+ carouselInitialized = true;
1091
+
1092
+ prevBtn.onclick = function(e) {
1093
+ e.preventDefault();
1094
+ e.stopPropagation();
1095
+ currentExample = (currentExample - 1 + totalExamples) % totalExamples;
1096
+ updateCarousel();
1097
+ };
1098
+
1099
+ nextBtn.onclick = function(e) {
1100
+ e.preventDefault();
1101
+ e.stopPropagation();
1102
+ currentExample = (currentExample + 1) % totalExamples;
1103
+ updateCarousel();
1104
+ };
1105
+
1106
+ dots.forEach((dot, index) => {
1107
+ dot.onclick = function(e) {
1108
+ e.preventDefault();
1109
+ e.stopPropagation();
1110
+ currentExample = index;
1111
+ updateCarousel();
1112
+ };
1113
+ });
1114
+
1115
+ items.forEach((item, index) => {
1116
+ item.onclick = function(e) {
1117
+ e.preventDefault();
1118
+ e.stopPropagation();
1119
+ const text = this.getAttribute('data-text');
1120
+ if (text) {
1121
+ setExample(text);
1122
+ }
1123
+ };
1124
+ });
1125
+ }
1126
+
1127
+ function isAutoScrollEnabled() {
1128
+ const checkbox = document.querySelector('#auto-scroll-checkbox input[type="checkbox"]');
1129
+ return checkbox ? checkbox.checked : true;
1130
+ }
1131
+
1132
+ function scrollToBottom() {
1133
+ if (!isAutoScrollEnabled()) return;
1134
+ const outputArea = document.querySelector('#output-area');
1135
+ if (outputArea) {
1136
+ // 直接滚动 #output-area
1137
+ outputArea.scrollTop = outputArea.scrollHeight;
1138
+ }
1139
+ }
1140
+
1141
+ // 监听输出区域的内容变化,自动滚动
1142
+ function setupAutoScroll() {
1143
+ const outputArea = document.querySelector('#output-area');
1144
+ if (outputArea) {
1145
+ const observer = new MutationObserver(function() {
1146
+ // 延迟滚动以确保 DOM 已更新
1147
+ requestAnimationFrame(function() {
1148
+ setTimeout(scrollToBottom, 50);
1149
+ });
1150
+ });
1151
+ observer.observe(outputArea, { childList: true, subtree: true, characterData: true });
1152
+ }
1153
+ }
1154
+
1155
+ function updateOutputVisibility() {
1156
+ const outputArea = document.getElementById('output-area');
1157
+ if (outputArea) {
1158
+ const content = outputArea.innerHTML.trim();
1159
+ // 检查是否有实际内容(不只是空的 div 或空白)
1160
+ const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
1161
+ if (hasContent) {
1162
+ outputArea.classList.remove('hidden-output');
1163
+ outputArea.classList.add('has-content');
1164
+ outputArea.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important; height: 50vh !important; min-height: 250px !important; max-height: 50vh !important; overflow-y: scroll !important; padding: 1rem !important; border: 1px solid #e5e7eb !important; border-radius: 8px !important; background: #fafafa !important;';
1165
+ } else {
1166
+ outputArea.classList.add('hidden-output');
1167
+ outputArea.classList.remove('has-content');
1168
+ outputArea.style.cssText = 'display: none !important; visibility: hidden !important; opacity: 0 !important; height: 0 !important; min-height: 0 !important; max-height: 0 !important;';
1169
+ }
1170
+ }
1171
+ }
1172
+
1173
+ function initLayout() {
1174
+ if (layoutInitialized) return;
1175
+
1176
+ const mainContent = document.getElementById('main-content');
1177
+ const outputArea = document.getElementById('output-area');
1178
+
1179
+ if (!mainContent) return;
1180
+
1181
+ layoutInitialized = true;
1182
+ mainContent.classList.add('initial-state');
1183
+
1184
+ // 初始化时立即隐藏空的输出区域 - 使用内联样式确保生效
1185
+ if (outputArea) {
1186
+ const content = outputArea.innerHTML.trim();
1187
+ const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
1188
+ if (!hasContent) {
1189
+ outputArea.style.cssText = 'display: none !important; visibility: hidden !important; height: 0 !important; min-height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; opacity: 0 !important;';
1190
+ outputArea.classList.add('hidden-output');
1191
+ outputArea.classList.remove('has-content');
1192
+ }
1193
+ }
1194
+
1195
+ // 设置自动滚动监听
1196
+ setupAutoScroll();
1197
+
1198
+ const outputObserver = new MutationObserver(function() {
1199
+ const content = outputArea ? outputArea.innerHTML.trim() : '';
1200
+ const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
1201
+
1202
+ if (hasContent) {
1203
+ mainContent.classList.remove('initial-state');
1204
+ outputArea.classList.remove('hidden-output');
1205
+ outputArea.classList.add('has-content');
1206
+ outputArea.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important; height: 50vh !important; min-height: 250px !important; max-height: 50vh !important; overflow-y: scroll !important; padding: 1rem !important; border: 1px solid #e5e7eb !important; border-radius: 8px !important; background: #fafafa !important;';
1207
+ setTimeout(scrollToBottom, 100);
1208
+ } else {
1209
+ mainContent.classList.add('initial-state');
1210
+ if (outputArea) {
1211
+ outputArea.classList.add('hidden-output');
1212
+ outputArea.classList.remove('has-content');
1213
+ outputArea.style.cssText = 'display: none !important; visibility: hidden !important; opacity: 0 !important; height: 0 !important; min-height: 0 !important; max-height: 0 !important;';
1214
+ }
1215
+ }
1216
+ });
1217
+
1218
+ if (outputArea) {
1219
+ outputObserver.observe(outputArea, { childList: true, subtree: true, characterData: true });
1220
+ }
1221
+
1222
+ const questionInput = document.querySelector('#question-input textarea');
1223
+ if (questionInput) {
1224
+ questionInput.focus();
1225
+ }
1226
+ }
1227
+
1228
+ const observer = new MutationObserver(function(mutations, obs) {
1229
+ initCarousel();
1230
+ initLayout();
1231
+ if (carouselInitialized && layoutInitialized) {
1232
+ obs.disconnect();
1233
+ }
1234
+ });
1235
+
1236
+ observer.observe(document.body, {
1237
+ childList: true,
1238
+ subtree: true
1239
+ });
1240
+
1241
+ // 立即尝试隐藏输出区域(在 DOM 完全加载之前)
1242
+ function hideOutputAreaEarly() {
1243
+ const outputArea = document.getElementById('output-area');
1244
+ if (outputArea) {
1245
+ const content = outputArea.innerHTML.trim();
1246
+ const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
1247
+ if (!hasContent) {
1248
+ outputArea.style.cssText = 'display: none !important; visibility: hidden !important; height: 0 !important; min-height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; opacity: 0 !important;';
1249
+ outputArea.classList.add('hidden-output');
1250
+ outputArea.classList.remove('has-content');
1251
+ }
1252
+ }
1253
+ }
1254
+
1255
+ // 多次尝试隐藏,确保在各种时机都能生效
1256
+ hideOutputAreaEarly();
1257
+ document.addEventListener('DOMContentLoaded', hideOutputAreaEarly);
1258
+ setTimeout(hideOutputAreaEarly, 0);
1259
+ setTimeout(hideOutputAreaEarly, 100);
1260
+ setTimeout(hideOutputAreaEarly, 300);
1261
+ setTimeout(hideOutputAreaEarly, 500);
1262
+ setTimeout(function() { initCarousel(); initLayout(); }, 1000);
1263
+ setTimeout(function() { initCarousel(); initLayout(); }, 2000);
1264
+
1265
+ // 深色模式适配 - 动态更新 output-area 背景色(防闪烁优化版)
1266
+ let lastUpdateTime = 0;
1267
+ const UPDATE_THROTTLE = 50; // 最小更新间隔 50ms
1268
+
1269
+ function updateOutputAreaDarkMode() {
1270
+ const now = Date.now();
1271
+ if (now - lastUpdateTime < UPDATE_THROTTLE) {
1272
+ return; // 跳过过于频繁的更新
1273
+ }
1274
+ lastUpdateTime = now;
1275
+
1276
+ const outputArea = document.getElementById('output-area');
1277
+ if (!outputArea) return;
1278
+
1279
+ // 检查是否是深色模式
1280
+ const isDark = document.documentElement.classList.contains('dark') ||
1281
+ document.body.classList.contains('dark') ||
1282
+ window.matchMedia('(prefers-color-scheme: dark)').matches;
1283
+
1284
+ if (isDark) {
1285
+ // 深色模式:深色背景
1286
+ outputArea.style.setProperty('background', '#111827', 'important');
1287
+ outputArea.style.setProperty('border-color', '#374151', 'important');
1288
+ } else {
1289
+ // 浅色模式:浅色背景
1290
+ outputArea.style.setProperty('background', '#fafafa', 'important');
1291
+ outputArea.style.setProperty('border-color', '#e5e7eb', 'important');
1292
+ }
1293
+ }
1294
+
1295
+ // 延迟初始化,避免页面加载时闪烁
1296
+ setTimeout(function() {
1297
+ updateOutputAreaDarkMode();
1298
+
1299
+ // 监听深色模式变化
1300
+ const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
1301
+ darkModeMediaQuery.addEventListener('change', updateOutputAreaDarkMode);
1302
+
1303
+ // 监听 DOM class 变化
1304
+ const darkModeObserver = new MutationObserver(updateOutputAreaDarkMode);
1305
+ darkModeObserver.observe(document.documentElement, {
1306
+ attributes: true,
1307
+ attributeFilter: ['class']
1308
+ });
1309
+ darkModeObserver.observe(document.body, {
1310
+ attributes: true,
1311
+ attributeFilter: ['class']
1312
+ });
1313
+
1314
+ // 监听 output-area 的内容变化
1315
+ const outputArea = document.getElementById('output-area');
1316
+ if (outputArea) {
1317
+ const outputContentObserver = new MutationObserver(function() {
1318
+ // 内容变化时立即应用深色模式样式
1319
+ requestAnimationFrame(updateOutputAreaDarkMode);
1320
+ });
1321
+
1322
+ outputContentObserver.observe(outputArea, {
1323
+ childList: true,
1324
+ subtree: true
1325
+ });
1326
+ }
1327
+ }, 500); // 延迟 500ms 后再启动监听
1328
+ })();
1329
+ """
1330
+
1331
+ def create_interface():
1332
+ # Get the directory where this script is located for static files
1333
+ script_dir = os.path.dirname(os.path.abspath(__file__))
1334
+
1335
+ # Helper function to convert image to base64 for embedding in HTML
1336
+ def image_to_base64(image_path):
1337
+ """Convert image file to base64 string for HTML embedding."""
1338
+ try:
1339
+ with open(image_path, 'rb') as img_file:
1340
+ img_data = img_file.read()
1341
+ b64_string = base64.b64encode(img_data).decode('utf-8')
1342
+ # Determine MIME type based on file extension
1343
+ ext = image_path.lower().split('.')[-1]
1344
+ mime_types = {
1345
+ 'png': 'image/png',
1346
+ 'jpg': 'image/jpeg',
1347
+ 'jpeg': 'image/jpeg',
1348
+ 'svg': 'image/svg+xml',
1349
+ 'gif': 'image/gif'
1350
+ }
1351
+ mime_type = mime_types.get(ext, 'image/png')
1352
+ return f"data:{mime_type};base64,{b64_string}"
1353
+ except Exception as e:
1354
+ print(f"Error loading image {image_path}: {e}")
1355
+ return ""
1356
+
1357
+ # Inline CSS - all styles embedded directly
1358
+ INLINE_CSS = """
1359
+ /* Global Styles */
1360
+ .gradio-container {
1361
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
1362
+ }
1363
+
1364
+ /* Thinking Styles */
1365
+ .thinking-collapsed {
1366
+ background: #f9fafb;
1367
+ border: 1px solid #e5e7eb;
1368
+ border-radius: 8px;
1369
+ padding: 0.75rem;
1370
+ margin: 0.5rem 0;
1371
+ }
1372
+
1373
+ .thinking-collapsed summary {
1374
+ cursor: pointer;
1375
+ font-weight: 500;
1376
+ color: #6b7280;
1377
+ font-size: 0.875rem;
1378
+ }
1379
+
1380
+ .thinking-collapsed summary:hover {
1381
+ color: #374151;
1382
+ }
1383
+
1384
+ .thinking-content {
1385
+ margin-top: 0.5rem;
1386
+ color: #374151;
1387
+ white-space: pre-wrap;
1388
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
1389
+ font-size: 0.875rem;
1390
+ line-height: 1.5;
1391
+ }
1392
+
1393
+ .thinking-streaming {
1394
+ background: #f0f9ff;
1395
+ border: 1px solid #bae6fd;
1396
+ border-radius: 8px;
1397
+ padding: 0.875rem;
1398
+ margin: 0.5rem 0;
1399
+ color: #0c4a6e;
1400
+ white-space: pre-wrap;
1401
+ font-family: 'SF Mono', Monaco, monospace;
1402
+ font-size: 0.875rem;
1403
+ line-height: 1.5;
1404
+ }
1405
+
1406
+ /* Tool Call Card */
1407
+ .tool-call-card {
1408
+ background: #f9fafb;
1409
+ border-radius: 8px;
1410
+ padding: 1rem;
1411
+ margin: 0.75rem 0;
1412
+ }
1413
+
1414
+ .tool-info {
1415
+ display: flex;
1416
+ flex-direction: column;
1417
+ gap: 0.25rem;
1418
+ }
1419
+
1420
+ .tool-name {
1421
+ font-weight: 600;
1422
+ color: #374151;
1423
+ font-size: 0.875rem;
1424
+ }
1425
+
1426
+ .tool-detail {
1427
+ color: #6b7280;
1428
+ font-size: 0.8rem;
1429
+ }
1430
+
1431
+ /* Result Card */
1432
+ .result-card-expanded {
1433
+ background: white;
1434
+ border-radius: 8px;
1435
+ padding: 1.25rem;
1436
+ margin: 1rem 0;
1437
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
1438
+ }
1439
+
1440
+ .result-header-expanded {
1441
+ font-weight: 600;
1442
+ color: #374151;
1443
+ margin-bottom: 1.25rem;
1444
+ padding-bottom: 0.5rem;
1445
+ border-bottom: 1px solid #e5e7eb;
1446
+ font-size: 1rem;
1447
+ }
1448
+
1449
+ .result-content-expanded {
1450
+ color: #4b5563;
1451
+ line-height: 1.6;
1452
+ }
1453
+
1454
+ .result-content-expanded p {
1455
+ margin: 0.5rem 0;
1456
+ }
1457
+
1458
+ .result-content-expanded code {
1459
+ background: #f3f4f6;
1460
+ padding: 0.125rem 0.375rem;
1461
+ border-radius: 3px;
1462
+ font-family: monospace;
1463
+ font-size: 0.875em;
1464
+ }
1465
+
1466
+ /* Search Result Card Hover */
1467
+ .search-result-card {
1468
+ transition: all 0.2s ease;
1469
+ }
1470
+
1471
+ .search-result-card:hover {
1472
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15) !important;
1473
+ border-color: #667eea !important;
1474
+ }
1475
+
1476
+ /* Answer Section */
1477
+ .answer-section {
1478
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
1479
+ border-left: 4px solid #10a37f;
1480
+ border-radius: 8px;
1481
+ padding: 1.5rem;
1482
+ margin: 1rem 0;
1483
+ }
1484
+
1485
+ .answer-section p {
1486
+ color: #374151;
1487
+ line-height: 1.7;
1488
+ margin: 0.5rem 0;
1489
+ }
1490
+
1491
+ .answer-section strong {
1492
+ color: #1e293b;
1493
+ font-weight: 600;
1494
+ }
1495
+
1496
+ .answer-section a {
1497
+ color: #10a37f;
1498
+ text-decoration: none;
1499
+ font-weight: 500;
1500
+ }
1501
+
1502
+ .answer-section a:hover {
1503
+ text-decoration: underline;
1504
+ }
1505
+
1506
+ /* User Message Bubble - 淡蓝色背景,右对齐 */
1507
+ .user-message-bubble {
1508
+ background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
1509
+ color: #0c4a6e;
1510
+ border-radius: 1rem 1rem 0.25rem 1rem;
1511
+ padding: 1rem 1.25rem;
1512
+ margin: 1rem 0 1rem auto;
1513
+ max-width: 80%;
1514
+ box-shadow: 0 2px 8px rgba(14, 165, 233, 0.2);
1515
+ border: 1px solid #7dd3fc;
1516
+ text-align: left;
1517
+ }
1518
+
1519
+ /* Reference Section Collapsible */
1520
+ .reference-section {
1521
+ margin-top: 40px;
1522
+ border-top: 1px solid #e5e7eb;
1523
+ padding-top: 20px;
1524
+ }
1525
+
1526
+ .reference-title {
1527
+ font-size: 1.2rem;
1528
+ font-weight: 600;
1529
+ margin-bottom: 16px;
1530
+ color: #111827;
1531
+ cursor: pointer;
1532
+ outline: none;
1533
+ }
1534
+
1535
+ .ref-url {
1536
+ text-align: left !important;
1537
+ }
1538
+
1539
+ .user-message-content {
1540
+ line-height: 1.6;
1541
+ font-size: 0.95rem;
1542
+ }
1543
+
1544
+ /* Output area - 固定高度可滚动 */
1545
+ /* 强制固定高度,内容在里面滚动 */
1546
+ #output-area,
1547
+ #output-area.output-box,
1548
+ div#output-area {
1549
+ height: 50vh !important;
1550
+ min-height: 250px !important;
1551
+ max-height: 50vh !important;
1552
+ overflow-y: scroll !important;
1553
+ overflow-x: hidden !important;
1554
+ padding: 1rem !important;
1555
+ border: 1px solid #e5e7eb !important;
1556
+ border-radius: 8px !important;
1557
+ background: #fafafa !important;
1558
+ scroll-behavior: smooth;
1559
+ flex-shrink: 0 !important;
1560
+ flex-grow: 0 !important;
1561
+ }
1562
+
1563
+ /* 内部所有元素不能撑破容器 */
1564
+ #output-area * {
1565
+ max-height: none !important;
1566
+ overflow: visible !important;
1567
+ }
1568
+
1569
+ #output-area > div {
1570
+ height: auto !important;
1571
+ max-height: none !important;
1572
+ overflow: visible !important;
1573
+ border: none !important;
1574
+ background: transparent !important;
1575
+ padding: 0 !important;
1576
+ margin: 0 !important;
1577
+ box-shadow: none !important;
1578
+ }
1579
+
1580
+ /* 初始状态和空内容时隐藏 output-area */
1581
+ #output-area:empty,
1582
+ #output-area.hidden-output,
1583
+ #output-area:not(.has-content),
1584
+ .hidden-output#output-area,
1585
+ div.hidden-output#output-area,
1586
+ #main-content #output-area.hidden-output,
1587
+ #main-content .output-box.hidden-output {
1588
+ display: none !important;
1589
+ visibility: hidden !important;
1590
+ height: 0 !important;
1591
+ min-height: 0 !important;
1592
+ max-height: 0 !important;
1593
+ padding: 0 !important;
1594
+ margin: 0 !important;
1595
+ border: none !important;
1596
+ opacity: 0 !important;
1597
+ overflow: hidden !important;
1598
+ }
1599
+
1600
+ /* 防止内部内容撑破容器 */
1601
+ #output-area > * {
1602
+ max-width: 100%;
1603
+ word-wrap: break-word;
1604
+ overflow-wrap: break-word;
1605
+ }
1606
+
1607
+ /* 主内容区域布局 - 限制整体高度 */
1608
+ #main-content {
1609
+ display: flex !important;
1610
+ flex-direction: column !important;
1611
+ height: auto !important;
1612
+ max-height: none !important;
1613
+ overflow: visible !important;
1614
+ }
1615
+
1616
+ /* Gradio 包装容器限制 */
1617
+ #main-content > div {
1618
+ flex-shrink: 0;
1619
+ }
1620
+
1621
+ #output-area::-webkit-scrollbar {
1622
+ width: 8px;
1623
+ }
1624
+
1625
+ #output-area::-webkit-scrollbar-track {
1626
+ background: #f1f1f1;
1627
+ border-radius: 4px;
1628
+ }
1629
+
1630
+ #output-area::-webkit-scrollbar-thumb {
1631
+ background: #c1c1c1;
1632
+ border-radius: 4px;
1633
+ }
1634
+
1635
+ #output-area::-webkit-scrollbar-thumb:hover {
1636
+ background: #a1a1a1;
1637
+ }
1638
+
1639
+ /* 自动滚动控制按钮样式 */
1640
+ #auto-scroll-checkbox {
1641
+ margin-top: 0.5rem;
1642
+ }
1643
+
1644
+ #auto-scroll-checkbox label {
1645
+ font-size: 0.85rem;
1646
+ color: #4b5563;
1647
+ }
1648
+
1649
+ /* Completion Message */
1650
+ .completion-msg {
1651
+ text-align: center;
1652
+ color: #10a37f;
1653
+ font-weight: 600;
1654
+ padding: 1rem;
1655
+ margin: 1rem 0;
1656
+ background: #f0fdf4;
1657
+ border-radius: 8px;
1658
+ border: 1px solid #86efac;
1659
+ }
1660
+
1661
+ /* Error Message */
1662
+ .error-message {
1663
+ background: #fee2e2;
1664
+ border-left: 4px solid #dc2626;
1665
+ border-radius: 8px;
1666
+ padding: 1rem;
1667
+ margin: 1rem 0;
1668
+ color: #991b1b;
1669
+ }
1670
+
1671
+ .error-message strong {
1672
+ color: #7f1d1d;
1673
+ }
1674
+
1675
+ .error-message a {
1676
+ color: #dc2626;
1677
+ font-weight: 500;
1678
+ }
1679
+
1680
+ /* Round Badge - 淡蓝色背景 */
1681
+ .round-badge {
1682
+ display: inline-block;
1683
+ background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
1684
+ color: #0369a1;
1685
+ padding: 0.25rem 0.75rem;
1686
+ border-radius: 999px;
1687
+ font-size: 0.75rem;
1688
+ font-weight: 600;
1689
+ margin: 0.5rem 0;
1690
+ box-shadow: 0 2px 4px rgba(14, 165, 233, 0.15);
1691
+ border: 1px solid #7dd3fc;
1692
+ }
1693
+
1694
+ /* Settings Section */
1695
+ #settings-group {
1696
+ background: transparent !important;
1697
+ border: none !important;
1698
+ padding: 0 !important;
1699
+ box-shadow: none !important;
1700
+ gap: 0 !important;
1701
+ }
1702
+
1703
+ #max-rounds-slider, #auto-scroll-checkbox {
1704
+ background: #f9fafb;
1705
+ border: 1px solid #e5e7eb;
1706
+ border-radius: 6px;
1707
+ padding: 0.75rem;
1708
+ margin-bottom: 0.5rem;
1709
+ }
1710
+
1711
+ .settings-header {
1712
+ display: flex;
1713
+ align-items: center;
1714
+ gap: 0.5rem;
1715
+ margin-bottom: 0.875rem;
1716
+ }
1717
+
1718
+ .settings-title {
1719
+ font-size: 0.875rem;
1720
+ font-weight: 600;
1721
+ color: #374151;
1722
+ }
1723
+
1724
+ .settings-api-row {
1725
+ display: flex;
1726
+ align-items: center;
1727
+ justify-content: space-between;
1728
+ margin-bottom: 0.375rem;
1729
+ }
1730
+
1731
+ .settings-label {
1732
+ font-size: 0.8rem;
1733
+ font-weight: 500;
1734
+ color: #4b5563;
1735
+ }
1736
+
1737
+ .settings-help-link {
1738
+ display: inline-flex;
1739
+ align-items: center;
1740
+ gap: 0.25rem;
1741
+ font-size: 0.7rem;
1742
+ color: #667eea;
1743
+ text-decoration: none;
1744
+ transition: opacity 0.2s;
1745
+ }
1746
+
1747
+ .settings-help-icon {
1748
+ display: inline-flex;
1749
+ align-items: center;
1750
+ justify-content: center;
1751
+ width: 14px;
1752
+ height: 14px;
1753
+ border-radius: 50%;
1754
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1755
+ color: white;
1756
+ font-size: 0.6rem;
1757
+ font-weight: bold;
1758
+ }
1759
+
1760
+ /* Tools Section */
1761
+ .tools-section {
1762
+ margin-top: 0;
1763
+ }
1764
+
1765
+ .tools-title {
1766
+ font-size: 0.875rem;
1767
+ font-weight: 600;
1768
+ color: #374151;
1769
+ margin-bottom: 0.5rem;
1770
+ margin-top: 0;
1771
+ }
1772
+
1773
+ /* Tool Item */
1774
+ .tool-item {
1775
+ background: #f9fafb;
1776
+ padding: 0.75rem;
1777
+ border-radius: 6px;
1778
+ margin-bottom: 0.5rem;
1779
+ border: 1px solid #e5e7eb;
1780
+ }
1781
+
1782
+ .tool-item strong {
1783
+ color: #374151;
1784
+ font-size: 0.85rem;
1785
+ }
1786
+
1787
+ .tool-item span {
1788
+ color: #6b7280;
1789
+ font-size: 0.8rem;
1790
+ }
1791
+
1792
+ /* Examples Section */
1793
+ .examples-section {
1794
+ margin-top: -0.5rem;
1795
+ }
1796
+
1797
+ .examples-title {
1798
+ font-size: 0.875rem;
1799
+ font-weight: 600;
1800
+ color: #374151;
1801
+ margin-bottom: 0.5rem;
1802
+ }
1803
+
1804
+ /* Example Carousel */
1805
+ .example-carousel {
1806
+ background: white;
1807
+ border-radius: 8px;
1808
+ padding: 1rem;
1809
+ border: 1px solid #e5e7eb;
1810
+ }
1811
+
1812
+ .carousel-container {
1813
+ position: relative;
1814
+ min-height: 60px;
1815
+ margin-bottom: 0.75rem;
1816
+ }
1817
+
1818
+ .carousel-item {
1819
+ display: none;
1820
+ opacity: 0;
1821
+ transition: opacity 0.3s ease;
1822
+ }
1823
+
1824
+ .carousel-item.active {
1825
+ display: block;
1826
+ opacity: 1;
1827
+ }
1828
+
1829
+ .carousel-item-text {
1830
+ background: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
1831
+ padding: 1rem;
1832
+ border-radius: 6px;
1833
+ color: #374151;
1834
+ font-size: 0.875rem;
1835
+ line-height: 1.5;
1836
+ border: 1px solid #e0e7ff;
1837
+ }
1838
+
1839
+ .carousel-controls {
1840
+ display: flex;
1841
+ align-items: center;
1842
+ justify-content: center;
1843
+ gap: 1rem;
1844
+ }
1845
+
1846
+ .carousel-btn {
1847
+ cursor: pointer;
1848
+ width: 32px;
1849
+ height: 32px;
1850
+ border-radius: 50%;
1851
+ background: #f3f4f6;
1852
+ display: flex;
1853
+ align-items: center;
1854
+ justify-content: center;
1855
+ font-size: 1.25rem;
1856
+ color: #6b7280;
1857
+ transition: all 0.2s ease;
1858
+ user-select: none;
1859
+ }
1860
+
1861
+ .carousel-btn:hover {
1862
+ background: #667eea;
1863
+ color: white;
1864
+ }
1865
+
1866
+ .carousel-indicators {
1867
+ display: flex;
1868
+ gap: 0.5rem;
1869
+ }
1870
+
1871
+ .carousel-dot {
1872
+ width: 8px;
1873
+ height: 8px;
1874
+ border-radius: 50%;
1875
+ background: #d1d5db;
1876
+ cursor: pointer;
1877
+ transition: all 0.2s ease;
1878
+ }
1879
+
1880
+ .carousel-dot.active {
1881
+ background: #667eea;
1882
+ width: 24px;
1883
+ border-radius: 4px;
1884
+ }
1885
+
1886
+ /* Welcome Message */
1887
+ .welcome-container {
1888
+ text-align: center;
1889
+ padding: 3rem 2rem;
1890
+ }
1891
+
1892
+ .welcome-title {
1893
+ font-size: 2rem;
1894
+ font-weight: 700;
1895
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1896
+ -webkit-background-clip: text;
1897
+ -webkit-text-fill-color: transparent;
1898
+ background-clip: text;
1899
+ margin-bottom: 1rem;
1900
+ }
1901
+
1902
+ .welcome-subtitle {
1903
+ color: #6b7280;
1904
+ font-size: 1.1rem;
1905
+ margin-bottom: 2rem;
1906
+ }
1907
+
1908
+ /* Footer */
1909
+ .footer-container {
1910
+ text-align: center;
1911
+ padding: 1.5rem;
1912
+ color: #9ca3af;
1913
+ font-size: 0.875rem;
1914
+ border-top: 1px solid #e5e7eb;
1915
+ margin-top: 2rem;
1916
+ }
1917
+
1918
+ .footer-container a {
1919
+ color: #667eea;
1920
+ text-decoration: none;
1921
+ }
1922
+
1923
+ .footer-container a:hover {
1924
+ text-decoration: underline;
1925
+ }
1926
+
1927
+ /* Disclaimer */
1928
+ .disclaimer {
1929
+ text-align: center;
1930
+ padding: 1rem;
1931
+ color: #6b7280;
1932
+ font-size: 0.875rem;
1933
+ border-top: 1px solid #e5e7eb;
1934
+ margin-top: 1rem;
1935
+ }
1936
+
1937
+ /* ========== 深色模式适配 ========== */
1938
+ @media (prefers-color-scheme: dark) {
1939
+ /* Settings 区域 */
1940
+ #settings-group {
1941
+ background: transparent !important;
1942
+ border: none !important;
1943
+ padding: 0 !important;
1944
+ gap: 0 !important;
1945
+ }
1946
+
1947
+ #max-rounds-slider, #auto-scroll-checkbox {
1948
+ background: #1f2937 !important;
1949
+ border: 1px solid #374151 !important;
1950
+ border-radius: 6px !important;
1951
+ padding: 0.75rem !important;
1952
+ }
1953
+
1954
+ .settings-title,
1955
+ .tools-title,
1956
+ .examples-title {
1957
+ color: #e5e7eb !important;
1958
+ }
1959
+
1960
+ .settings-label {
1961
+ color: #9ca3af !important;
1962
+ }
1963
+
1964
+ .settings-help-link {
1965
+ color: #818cf8 !important;
1966
+ }
1967
+
1968
+ /* Available Tools 区域 */
1969
+ .tool-item {
1970
+ background: #1f2937 !important;
1971
+ border-color: #374151 !important;
1972
+ }
1973
+
1974
+ .tool-item strong {
1975
+ color: #e5e7eb !important;
1976
+ }
1977
+
1978
+ .tool-item span {
1979
+ color: #9ca3af !important;
1980
+ }
1981
+
1982
+ /* Example Carousel 区域 */
1983
+ .example-carousel {
1984
+ background: #1f2937 !important;
1985
+ border-color: #374151 !important;
1986
+ }
1987
+
1988
+ .carousel-item-text {
1989
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
1990
+ border-color: #3b82f6 !important;
1991
+ color: #e0f2fe !important;
1992
+ }
1993
+
1994
+ .carousel-btn {
1995
+ background: #374151 !important;
1996
+ color: #9ca3af !important;
1997
+ }
1998
+
1999
+ .carousel-btn:hover {
2000
+ background: #667eea !important;
2001
+ color: white !important;
2002
+ }
2003
+
2004
+ .carousel-dot {
2005
+ background: #4b5563 !important;
2006
+ }
2007
+
2008
+ .carousel-dot.active {
2009
+ background: #667eea !important;
2010
+ }
2011
+
2012
+ /* Output area 深色模式 */
2013
+ #output-area,
2014
+ #output-area.output-box,
2015
+ div#output-area {
2016
+ background: #111827 !important;
2017
+ border: 1px solid #374151 !important;
2018
+ }
2019
+
2020
+ #output-area::-webkit-scrollbar-track {
2021
+ background: #1f2937 !important;
2022
+ }
2023
+
2024
+ #output-area::-webkit-scrollbar-thumb {
2025
+ background: #4b5563 !important;
2026
+ }
2027
+
2028
+ #output-area::-webkit-scrollbar-thumb:hover {
2029
+ background: #6b7280 !important;
2030
+ }
2031
+
2032
+ /* Tool call card 深色模式 */
2033
+ .tool-call-card {
2034
+ background: #1f2937 !important;
2035
+ }
2036
+
2037
+ .tool-name {
2038
+ color: #e5e7eb !important;
2039
+ }
2040
+
2041
+ .tool-detail {
2042
+ color: #9ca3af !important;
2043
+ }
2044
+
2045
+ /* Result card 深色模式 */
2046
+ .result-card-expanded {
2047
+ background: #1f2937 !important;
2048
+ }
2049
+
2050
+ .result-header-expanded {
2051
+ color: #e5e7eb !important;
2052
+ border-bottom-color: #374151 !important;
2053
+ }
2054
+
2055
+ .result-content-expanded {
2056
+ color: #d1d5db !important;
2057
+ }
2058
+
2059
+ /* Thinking 深色模式 */
2060
+ .thinking-collapsed {
2061
+ background: #1f2937 !important;
2062
+ border-color: #374151 !important;
2063
+ }
2064
+
2065
+ .thinking-collapsed summary {
2066
+ color: #9ca3af !important;
2067
+ }
2068
+
2069
+ .thinking-content {
2070
+ color: #d1d5db !important;
2071
+ }
2072
+
2073
+ .thinking-streaming {
2074
+ background: #0c4a6e !important;
2075
+ border-color: #0369a1 !important;
2076
+ color: #bae6fd !important;
2077
+ }
2078
+
2079
+ /* Answer section 深色模式 */
2080
+ .answer-section {
2081
+ background: linear-gradient(135deg, #0c4a6e 0%, #164e63 100%) !important;
2082
+ }
2083
+
2084
+ .answer-section p {
2085
+ color: #e0f2fe !important;
2086
+ }
2087
+
2088
+ .answer-section strong {
2089
+ color: #f0f9ff !important;
2090
+ }
2091
+
2092
+ /* 用户问题气泡深色模式 */
2093
+ .user-message-bubble {
2094
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
2095
+ color: #e0f2fe !important;
2096
+ border-color: #3b82f6 !important;
2097
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3) !important;
2098
+ }
2099
+
2100
+ .user-message-content {
2101
+ color: #e0f2fe !important;
2102
+ }
2103
+
2104
+ /* Round Badge 深色模式 */
2105
+ .round-badge {
2106
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
2107
+ color: #93c5fd !important;
2108
+ border-color: #3b82f6 !important;
2109
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2) !important;
2110
+ }
2111
+
2112
+ /* 搜索结果卡片深色模式 */
2113
+ .search-result-card {
2114
+ background: #1f2937 !important;
2115
+ border-color: #374151 !important;
2116
+ }
2117
+
2118
+ .search-result-card:hover {
2119
+ border-color: #667eea !important;
2120
+ }
2121
+
2122
+ /* 工具结果标题区域深色模式 */
2123
+ .result-card-expanded div[style*="background: linear-gradient"] {
2124
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
2125
+ border-color: #3b82f6 !important;
2126
+ }
2127
+
2128
+ .result-card-expanded div[style*="background: linear-gradient"] span[style*="color: #1e40af"],
2129
+ .result-card-expanded div[style*="background: linear-gradient"] a[style*="color: #1e40af"] {
2130
+ color: #93c5fd !important;
2131
+ }
2132
+
2133
+ .result-card-expanded div[style*="color: #64748b"] {
2134
+ color: #9ca3af !important;
2135
+ }
2136
+
2137
+ /* 完成消息深色模式 */
2138
+ .completion-msg {
2139
+ background: #064e3b !important;
2140
+ border-color: #059669 !important;
2141
+ color: #6ee7b7 !important;
2142
+ }
2143
+
2144
+ /* 错误消息深色模式 */
2145
+ .error-message {
2146
+ background: #450a0a !important;
2147
+ border-color: #b91c1c !important;
2148
+ color: #fca5a5 !important;
2149
+ }
2150
+
2151
+ /* 侧边栏标题深色模式 */
2152
+ div[style*="font-weight: 600"][style*="color: #374151"] {
2153
+ color: #e5e7eb !important;
2154
+ }
2155
+
2156
+ /* Disclaimer 深色模式 */
2157
+ .disclaimer {
2158
+ color: #9ca3af !important;
2159
+ border-color: #374151 !important;
2160
+ }
2161
+ }
2162
+
2163
+ /* Gradio 深色主题类名适配 */
2164
+ .dark #settings-group,
2165
+ .dark .tool-item,
2166
+ .dark .example-carousel {
2167
+ background: #1f2937 !important;
2168
+ border-color: #374151 !important;
2169
+ }
2170
+
2171
+ .dark .settings-title,
2172
+ .dark .tools-title,
2173
+ .dark .examples-title,
2174
+ .dark .tool-item strong,
2175
+ .dark .result-header-expanded,
2176
+ .dark .tool-name {
2177
+ color: #e5e7eb !important;
2178
+ }
2179
+
2180
+ .dark .settings-label,
2181
+ .dark .tool-item span,
2182
+ .dark .tool-detail,
2183
+ .dark .thinking-collapsed summary {
2184
+ color: #9ca3af !important;
2185
+ }
2186
+
2187
+ .dark .settings-help-link {
2188
+ color: #818cf8 !important;
2189
+ }
2190
+
2191
+ .dark .carousel-item-text {
2192
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
2193
+ border-color: #3b82f6 !important;
2194
+ color: #e0f2fe !important;
2195
+ }
2196
+
2197
+ .dark #output-area,
2198
+ .dark #output-area.output-box,
2199
+ .dark div#output-area {
2200
+ background: #111827 !important;
2201
+ border: 1px solid #374151 !important;
2202
+ }
2203
+
2204
+ .dark .tool-call-card,
2205
+ .dark .result-card-expanded,
2206
+ .dark .thinking-collapsed {
2207
+ background: #1f2937 !important;
2208
+ border-color: #374151 !important;
2209
+ }
2210
+
2211
+ .dark .result-content-expanded,
2212
+ .dark .thinking-content {
2213
+ color: #d1d5db !important;
2214
+ }
2215
+
2216
+ .dark .answer-section {
2217
+ background: linear-gradient(135deg, #0c4a6e 0%, #164e63 100%) !important;
2218
+ }
2219
+
2220
+ .dark .answer-section p {
2221
+ color: #e0f2fe !important;
2222
+ }
2223
+
2224
+ .dark .search-result-card {
2225
+ background: #1f2937 !important;
2226
+ border-color: #374151 !important;
2227
+ }
2228
+
2229
+ .dark .completion-msg {
2230
+ background: #064e3b !important;
2231
+ border-color: #059669 !important;
2232
+ color: #6ee7b7 !important;
2233
+ }
2234
+
2235
+ /* 用户问题气泡 Gradio dark 模式 */
2236
+ .dark .user-message-bubble {
2237
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
2238
+ color: #e0f2fe !important;
2239
+ border-color: #3b82f6 !important;
2240
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3) !important;
2241
+ }
2242
+
2243
+ .dark .user-message-content {
2244
+ color: #e0f2fe !important;
2245
+ }
2246
+
2247
+ /* Round Badge Gradio dark 模式 */
2248
+ .dark .round-badge {
2249
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
2250
+ color: #93c5fd !important;
2251
+ border-color: #3b82f6 !important;
2252
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2) !important;
2253
+ }
2254
+
2255
+ /* 工具结果标题 Gradio dark 模式 */
2256
+ .dark .result-card-expanded div[style*="background: linear-gradient"] {
2257
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
2258
+ border-color: #3b82f6 !important;
2259
+ }
2260
+
2261
+ /* Disclaimer Gradio dark 模式 */
2262
+ .dark .disclaimer {
2263
+ color: #9ca3af !important;
2264
+ border-color: #374151 !important;
2265
+ }
2266
+ /* Reference Section */
2267
+ .reference-section {
2268
+ margin-top: 40px;
2269
+ border-top: 1px solid #e5e7eb;
2270
+ padding-top: 20px;
2271
+ }
2272
+ .reference-title {
2273
+ font-size: 1.2rem;
2274
+ font-weight: 600;
2275
+ margin-bottom: 16px;
2276
+ color: #111827;
2277
+ }
2278
+ .reference-item {
2279
+ display: block;
2280
+ background-color: #fff;
2281
+ border: 1px solid #e5e7eb;
2282
+ border-radius: 12px;
2283
+ padding: 12px 16px;
2284
+ margin-bottom: 12px;
2285
+ text-decoration: none;
2286
+ color: #374151;
2287
+ transition: all 0.2s;
2288
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
2289
+ }
2290
+ .reference-item:hover {
2291
+ border-color: #3b82f6;
2292
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
2293
+ transform: translateY(-1px);
2294
+ text-decoration: none;
2295
+ }
2296
+ .ref-number {
2297
+ display: inline-block;
2298
+ background-color: #eff6ff;
2299
+ color: #2563eb;
2300
+ font-weight: 600;
2301
+ padding: 2px 6px;
2302
+ border-radius: 6px;
2303
+ margin-right: 8px;
2304
+ font-size: 0.85em;
2305
+ }
2306
+ .ref-text {
2307
+ font-weight: 500;
2308
+ color: #1f2937;
2309
+ }
2310
+ .ref-url {
2311
+ display: block;
2312
+ margin-top: 4px;
2313
+ font-size: 0.8em;
2314
+ color: #6b7280;
2315
+ overflow: hidden;
2316
+ text-overflow: ellipsis;
2317
+ white-space: nowrap;
2318
+ }
2319
+
2320
+ /* 深色模式适配 Reference Section */
2321
+ @media (prefers-color-scheme: dark) {
2322
+ .reference-title {
2323
+ color: #e5e7eb !important;
2324
+ }
2325
+ .reference-item {
2326
+ background-color: #1f2937 !important;
2327
+ border-color: #374151 !important;
2328
+ color: #d1d5db !important;
2329
+ }
2330
+ .reference-item:hover {
2331
+ border-color: #667eea !important;
2332
+ }
2333
+ .ref-number {
2334
+ background-color: #1e3a5f !important;
2335
+ color: #93c5fd !important;
2336
+ }
2337
+ .ref-text {
2338
+ color: #e5e7eb !important;
2339
+ }
2340
+ .ref-url {
2341
+ color: #9ca3af !important;
2342
+ }
2343
+ }
2344
+
2345
+ /* Gradio dark 模式适配 Reference Section */
2346
+ .dark .reference-title {
2347
+ color: #e5e7eb !important;
2348
+ }
2349
+ .dark .reference-item {
2350
+ background-color: #1f2937 !important;
2351
+ border-color: #374151 !important;
2352
+ color: #d1d5db !important;
2353
+ }
2354
+ .dark .reference-item:hover {
2355
+ border-color: #667eea !important;
2356
+ }
2357
+ .dark .ref-number {
2358
+ background-color: #1e3a5f !important;
2359
+ color: #93c5fd !important;
2360
+ }
2361
+ .dark .ref-text {
2362
+ color: #e5e7eb !important;
2363
+ }
2364
+ .dark .ref-url {
2365
+ color: #9ca3af !important;
2366
+ }
2367
+ """
2368
+
2369
+ with gr.Blocks(css=INLINE_CSS, theme=gr.themes.Soft(), js=CAROUSEL_JS) as demo:
2370
+ # Header with logo and title images
2371
+ logo_path = os.path.join(script_dir, "or-logo1.png")
2372
+ title_path = os.path.join(script_dir, "openresearcher-title.svg")
2373
+
2374
+ logo_base64 = image_to_base64(logo_path)
2375
+ title_base64 = image_to_base64(title_path)
2376
+
2377
+ header_html = f"""
2378
+ <div style="
2379
+ text-align: center;
2380
+ padding: 0.5rem 1rem 0.5rem 1rem;
2381
+ background: transparent;
2382
+ display: flex;
2383
+ flex-direction: row;
2384
+ align-items: center;
2385
+ justify-content: center;
2386
+ gap: 1.5rem;
2387
+ ">
2388
+ """
2389
+
2390
+ if logo_base64:
2391
+ header_html += f'<img src="{logo_base64}" alt="OpenResearcher Logo" style="height: 84px;">'
2392
+ if title_base64:
2393
+ header_html += f'<img src="{title_base64}" alt="OpenResearcher" style="height: 84px;">'
2394
+
2395
+ header_html += "</div>"
2396
+
2397
+ gr.HTML(header_html)
2398
+
2399
+ gr.HTML("""
2400
+ <div style="display: flex; gap: 0px; justify-content: center; flex-wrap: wrap; margin-top: 0px; margin-bottom: 24px;">
2401
+ <a href="https://boiled-honeycup-4c7.notion.site/OpenResearcher-A-Fully-Open-Pipeline-for-Long-Horizon-Deep-Research-Trajectory-Synthesis-2f7e290627b5800cb3a0cd7e8d6ec0ea?source=copy_link" target="_blank">
2402
+ <img src="https://img.shields.io/badge/Blog-4285F4?style=for-the-badge&logo=google-chrome&logoColor=white" alt="Blog" style="height: 28px;">
2403
+ </a>
2404
+ <a href="https://github.com/TIGER-AI-Lab/OpenResearcher" target="_blank">
2405
+ <img src="https://img.shields.io/badge/Github-181717?style=for-the-badge&logo=github&logoColor=white" alt="Github" style="height: 28px;">
2406
+ </a>
2407
+ <a href="https://huggingface.co/datasets/OpenResearcher/OpenResearcher-Dataset" target="_blank">
2408
+ <img src="https://img.shields.io/badge/Dataset-FFB7B2?style=for-the-badge&logo=huggingface&logoColor=ffffff" alt="Dataset" style="height: 28px;">
2409
+ </a>
2410
+ <a href="https://huggingface.co/OpenResearcher/Nemotron-3-Nano-30B-A3B" target="_blank">
2411
+ <img src="https://img.shields.io/badge/Model-FFD966?style=for-the-badge&logo=huggingface&logoColor=ffffff" alt="Model" style="height: 28px;">
2412
+ </a>
2413
+ <a href="https://huggingface.co/datasets/OpenResearcher/OpenResearcher-Eval-Logs/tree/main" target="_blank">
2414
+ <img src="https://img.shields.io/badge/Eval%20Logs-755BB4?style=for-the-badge&logo=google-sheets&logoColor=white" alt="Eval Logs" style="height: 28px;">
2415
+ </a>
2416
+ </div>
2417
+ """)
2418
+
2419
+ # Main layout: Left sidebar + Right content
2420
+ with gr.Row():
2421
+ # Left Sidebar (settings & tools)
2422
+ with gr.Column(scale=1, min_width=280):
2423
+ # API Settings in a unified box
2424
+ with gr.Group(elem_id="settings-group"):
2425
+ gr.HTML('''
2426
+ <div class="settings-header">
2427
+ <span class="settings-title">⚙️ Settings</span>
2428
+ </div>
2429
+ ''')
2430
+ max_rounds_input = gr.Slider(
2431
+ minimum=1,
2432
+ maximum=200,
2433
+ value=50,
2434
+ step=1,
2435
+ label="Max Rounds",
2436
+ elem_id="max-rounds-slider"
2437
+ )
2438
+ auto_scroll_checkbox = gr.Checkbox(
2439
+ label="Auto Scroll",
2440
+ value=True,
2441
+ elem_id="auto-scroll-checkbox",
2442
+ interactive=True
2443
+ )
2444
+
2445
+ # Tools info
2446
+ gr.HTML("""
2447
+ <div class="tools-section">
2448
+ <div class="tools-title">🛠️ Available Tools</div>
2449
+ <div class="tool-item"><strong>browser.search</strong><br><span>Search the web</span></div>
2450
+ <div class="tool-item"><strong>browser.open</strong><br><span>Open & read pages</span></div>
2451
+ <div class="tool-item"><strong>browser.find</strong><br><span>Find text in page</span></div>
2452
+ </div>
2453
+ """)
2454
+
2455
+ # Example carousel with navigation
2456
+ gr.HTML("""
2457
+ <div class="examples-section">
2458
+ <div class="examples-title">💡 Try Examples</div>
2459
+ <div class="example-carousel" id="example-carousel">
2460
+ <div class="carousel-container">
2461
+ <div class="carousel-item active" data-index="0" data-text="Who won the Nobel Prize in Physics 2024?">
2462
+ <div class="carousel-item-text">🏆 Who won the Nobel Prize in Physics 2024?</div>
2463
+ </div>
2464
+ <div class="carousel-item" data-index="1" data-text="What are the latest breakthroughs in quantum computing in 2024?">
2465
+ <div class="carousel-item-text">🔬 What are the latest breakthroughs in quantum computing in 2024?</div>
2466
+ </div>
2467
+ <div class="carousel-item" data-index="2" data-text="What are the new features in Python 3.12?">
2468
+ <div class="carousel-item-text">🐍 What are the new features in Python 3.12?</div>
2469
+ </div>
2470
+ </div>
2471
+ <div class="carousel-controls">
2472
+ <div class="carousel-btn" id="prev-btn">‹</div>
2473
+ <div class="carousel-indicators">
2474
+ <div class="carousel-dot active" data-index="0"></div>
2475
+ <div class="carousel-dot" data-index="1"></div>
2476
+ <div class="carousel-dot" data-index="2"></div>
2477
+ </div>
2478
+ <div class="carousel-btn" id="next-btn">›</div>
2479
+ </div>
2480
+ </div>
2481
+ </div>
2482
+ """)
2483
+
2484
+ # Main content area (30-70)
2485
+ with gr.Column(scale=3, elem_id="main-content"):
2486
+ # Output area (on top, hidden initially)
2487
+ output_area = gr.HTML(
2488
+ value="",
2489
+ elem_classes=["output-box"],
2490
+ elem_id="output-area",
2491
+ visible=True
2492
+ )
2493
+
2494
+ # Welcome message (will be hidden after first search)
2495
+ welcome_html = gr.HTML(
2496
+ value="""
2497
+ <div id="welcome-section" class="welcome-section">
2498
+ <h2>What Would You Like to Research?</h2>
2499
+ <p>I am OpenResearcher, a leading open-source Deep Research Agent, welcome to try!</p>
2500
+ <p style="color: red;">Due to high traffic, if your submission has no response, please refresh the page and resubmit. Thank you!</p>
2501
+ </div>
2502
+ """,
2503
+ elem_id="welcome-container"
2504
+ )
2505
+
2506
+ # Input area at bottom
2507
+ question_input = gr.Textbox(
2508
+ label="",
2509
+ placeholder="Ask me anything and I'll handle the rest...",
2510
+ lines=2,
2511
+ show_label=False,
2512
+ elem_id="question-input",
2513
+ autofocus=True
2514
+ )
2515
+
2516
+ with gr.Row(elem_id="button-row"):
2517
+ submit_btn = gr.Button(
2518
+ "🔍 Start DeepResearch",
2519
+ variant="primary",
2520
+ elem_classes=["primary-btn"],
2521
+ scale=3
2522
+ )
2523
+ stop_btn = gr.Button("⏹ Stop", variant="stop", scale=1)
2524
+ clear_btn = gr.Button("🗑 Clear", scale=1)
2525
+
2526
+ # Function to hide welcome and show output
2527
+ async def start_research(question, max_rounds):
2528
+ # Generator that first hides welcome, then streams results
2529
+ # Also clears the input box for the next question
2530
+
2531
+ # Initial yield to immediately clear welcome, show loading in output, and clear input
2532
+ # IMPORTANT: Don't use empty string for output, or JS will hide the output area!
2533
+ yield "", '<div style="text-align: center; padding: 2rem; color: #6b7280;">Delving into it...</div>', ""
2534
+
2535
+ async for result in run_agent_streaming(question, max_rounds):
2536
+ yield "", result, ""
2537
+
2538
+ # Event handlers
2539
+ submit_event = submit_btn.click(
2540
+ fn=start_research,
2541
+ inputs=[question_input, max_rounds_input],
2542
+ outputs=[welcome_html, output_area, question_input],
2543
+ show_progress="hidden",
2544
+ concurrency_limit=20
2545
+ )
2546
+
2547
+ question_input.submit(
2548
+ fn=start_research,
2549
+ inputs=[question_input, max_rounds_input],
2550
+ outputs=[welcome_html, output_area, question_input],
2551
+ show_progress="hidden",
2552
+ concurrency_limit=20
2553
+ )
2554
+
2555
+ stop_btn.click(fn=None, inputs=None, outputs=None, cancels=[submit_event])
2556
+ clear_btn.click(
2557
+ fn=lambda: ("""
2558
+ <div id="welcome-section" class="welcome-section">
2559
+ <h2>What would you like to research?</h2>
2560
+ <p>Ask any question and I'll search the web to find answers</p>
2561
+ </div>
2562
+ """, "", ""),
2563
+ outputs=[welcome_html, output_area, question_input]
2564
+ )
2565
+
2566
+ # Disclaimer
2567
+ gr.HTML('''
2568
+ <div class="disclaimer">
2569
+ ⚠️ AI may generate incorrect information or citations. Please double-check important facts.
2570
+ </div>
2571
+ ''')
2572
+
2573
+ return demo
2574
+
2575
+ if __name__ == "__main__":
2576
+ print("="*60)
2577
+ print("OpenResearcher DeepSearch Agent - Helmholtz Blablador Provider")
2578
+ print("="*60)
2579
+ demo = create_interface()
2580
+ demo.queue(default_concurrency_limit=20).launch()