pikamomo commited on
Commit
a60c0af
·
1 Parent(s): 8c262e4

Initial deployment

Browse files
.dockerignore ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment files - DO NOT include in Docker image
2
+ .env
3
+ .env.local
4
+ .env.*.local
5
+ backend/.env
6
+ frontend/.env.local
7
+
8
+ # Python
9
+ __pycache__/
10
+ *.py[cod]
11
+ *$py.class
12
+ *.so
13
+ .Python
14
+ *.egg-info/
15
+ .eggs/
16
+ *.egg
17
+ .pytest_cache/
18
+ .mypy_cache/
19
+ .ruff_cache/
20
+ venv/
21
+ .venv/
22
+ ENV/
23
+
24
+ # Node.js
25
+ node_modules/
26
+ frontend/node_modules/
27
+ frontend/dist/
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+
32
+ # IDE
33
+ .idea/
34
+ .vscode/
35
+ *.swp
36
+ *.swo
37
+ *~
38
+
39
+ # Git
40
+ .git/
41
+ .gitignore
42
+
43
+ # OS
44
+ .DS_Store
45
+ Thumbs.db
46
+
47
+ # Notes (generated at runtime)
48
+ backend/src/notes/
49
+
50
+ # Documentation
51
+ *.md
52
+ !README.md
53
+
54
+ # Tests
55
+ tests/
56
+ *_test.py
57
+ test_*.py
.gitignore ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment files - NEVER commit these
2
+ .env
3
+ .env.local
4
+ .env.*.local
5
+ backend/.env
6
+ frontend/.env.local
7
+
8
+ # Python
9
+ __pycache__/
10
+ *.py[cod]
11
+ *$py.class
12
+ *.so
13
+ .Python
14
+ *.egg-info/
15
+ .eggs/
16
+ *.egg
17
+ .pytest_cache/
18
+ .mypy_cache/
19
+ .ruff_cache/
20
+ venv/
21
+ .venv/
22
+ ENV/
23
+ env/
24
+
25
+ # Node.js
26
+ node_modules/
27
+ npm-debug.log*
28
+ yarn-debug.log*
29
+ yarn-error.log*
30
+ .npm
31
+
32
+ # Build outputs
33
+ frontend/dist/
34
+ build/
35
+ dist/
36
+
37
+ # IDE
38
+ .idea/
39
+ .vscode/
40
+ *.swp
41
+ *.swo
42
+ *~
43
+ *.sublime-*
44
+
45
+ # OS
46
+ .DS_Store
47
+ .DS_Store?
48
+ ._*
49
+ .Spotlight-V100
50
+ .Trashes
51
+ ehthumbs.db
52
+ Thumbs.db
53
+
54
+ # Notes (generated at runtime)
55
+ backend/src/notes/*.md
56
+
57
+ # Logs
58
+ *.log
59
+ logs/
60
+
61
+ # Coverage
62
+ .coverage
63
+ htmlcov/
64
+ .tox/
65
+ .nox/
66
+
67
+ # Temporary files
68
+ *.tmp
69
+ *.temp
70
+ .cache/
Dockerfile ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # Stage 1: Build Frontend
3
+ # ============================================
4
+ FROM node:20-alpine AS frontend-builder
5
+
6
+ WORKDIR /app/frontend
7
+
8
+ # Copy package files
9
+ COPY frontend/package*.json ./
10
+
11
+ # Install dependencies
12
+ RUN npm ci
13
+
14
+ # Copy frontend source
15
+ COPY frontend/ ./
16
+
17
+ # Set API base URL to same origin (relative path)
18
+ ENV VITE_API_BASE_URL=""
19
+
20
+ # Build frontend
21
+ RUN npm run build
22
+
23
+ # ============================================
24
+ # Stage 2: Python Backend + Serve Frontend
25
+ # ============================================
26
+ FROM python:3.12-slim
27
+
28
+ WORKDIR /app
29
+
30
+ # Install system dependencies
31
+ RUN apt-get update && apt-get install -y --no-install-recommends \
32
+ curl \
33
+ && rm -rf /var/lib/apt/lists/*
34
+
35
+ # Copy backend requirements and install Python dependencies
36
+ COPY backend/pyproject.toml ./
37
+ RUN pip install --no-cache-dir .
38
+
39
+ # Copy backend source code
40
+ COPY backend/src/ ./src/
41
+
42
+ # Copy built frontend to serve as static files
43
+ COPY --from=frontend-builder /app/frontend/dist ./static
44
+
45
+ # Create notes directory
46
+ RUN mkdir -p ./src/notes
47
+
48
+ # Set working directory to src for imports
49
+ WORKDIR /app/src
50
+
51
+ # Expose port 7860 (Hugging Face Spaces default)
52
+ EXPOSE 7860
53
+
54
+ # Environment variables (will be overridden by HF Secrets)
55
+ ENV HOST=0.0.0.0
56
+ ENV PORT=7860
57
+ ENV CORS_ORIGINS="*"
58
+
59
+ # Start the application
60
+ CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,64 @@
1
  ---
2
  title: Deep Research Agent
3
- emoji: 🐠
4
- colorFrom: green
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Deep Research Agent
3
+ emoji: 🔍
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
  ---
10
 
11
+ # Deep Research Agent
12
+
13
+ A fully automated web research and summarization assistant powered by LangGraph.
14
+
15
+ ## Features
16
+
17
+ - 🔍 Intelligent web search using DuckDuckGo, Tavily, or Perplexity
18
+ - 📝 Automated research planning and execution
19
+ - 📊 Structured research reports with sources
20
+ - 🔄 Real-time streaming progress updates
21
+
22
+ ## Configuration
23
+
24
+ This Space requires the following secrets to be configured in the Settings:
25
+
26
+ ### Required Secrets
27
+
28
+ | Secret Name | Description |
29
+ |-------------|-------------|
30
+ | `LLM_PROVIDER` | LLM provider: `custom`, `ollama`, or `lmstudio` |
31
+ | `LLM_MODEL_ID` | Model name (e.g., `chatgpt-4o-latest`, `gpt-4-turbo`) |
32
+ | `LLM_API_KEY` | Your OpenAI API key |
33
+ | `LLM_BASE_URL` | API base URL (e.g., `https://api.openai.com/v1`) |
34
+
35
+ ### Optional Secrets
36
+
37
+ | Secret Name | Description | Default |
38
+ |-------------|-------------|---------|
39
+ | `SEARCH_API` | Search backend: `duckduckgo`, `tavily`, `perplexity`, `searxng` | `duckduckgo` |
40
+ | `TAVILY_API_KEY` | Tavily API key (if using Tavily search) | - |
41
+ | `PERPLEXITY_API_KEY` | Perplexity API key (if using Perplexity search) | - |
42
+ | `MAX_WEB_RESEARCH_LOOPS` | Maximum research iterations | `3` |
43
+ | `FETCH_FULL_PAGE` | Fetch full page content | `True` |
44
+ | `LLM_TIMEOUT` | API timeout in seconds | `60` |
45
+
46
+ ## Usage
47
+
48
+ 1. Enter your research topic in the input field
49
+ 2. Click "Start Research" to begin
50
+ 3. Watch the real-time progress as the agent:
51
+ - Plans research tasks
52
+ - Searches the web
53
+ - Summarizes findings
54
+ - Generates a comprehensive report
55
+
56
+ ## Tech Stack
57
+
58
+ - **Backend**: FastAPI + LangGraph + LangChain
59
+ - **Frontend**: Vue.js 3 + Vite
60
+ - **Search**: DuckDuckGo (default) / Tavily / Perplexity
61
+
62
+ ## License
63
+
64
+ MIT License
backend/pyproject.toml ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "langgraph-deep-researcher"
3
+ version = "0.1.0"
4
+ description = "Fully local web research and summarization assistant powered by LangGraph."
5
+ authors = [
6
+ { name = "Lance Martin" }
7
+ ]
8
+ license = "MIT"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "fastapi>=0.115.0",
12
+ "langgraph>=0.2.0",
13
+ "langchain>=0.3.0",
14
+ "langchain-openai>=0.2.0",
15
+ "langchain-community>=0.3.0",
16
+ "tavily-python>=0.5.0",
17
+ "python-dotenv==1.0.1",
18
+ "requests>=2.31.0",
19
+ "openai>=1.12.0",
20
+ "uvicorn[standard]>=0.32.0",
21
+ "ddgs>=9.6.1",
22
+ "loguru>=0.7.3",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ dev = ["mypy>=1.11.1", "ruff>=0.6.1"]
27
+
28
+ [build-system]
29
+ requires = ["setuptools>=73.0.0", "wheel"]
30
+ build-backend = "setuptools.build_meta"
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+
35
+ [tool.setuptools.package-data]
36
+ "*" = ["py.typed"]
37
+
38
+ [tool.ruff]
39
+ lint.select = [
40
+ "E", # pycodestyle
41
+ "F", # pyflakes
42
+ "I", # isort
43
+ "D", # pydocstyle
44
+ "D401", # First line should be in imperative mood
45
+ "T201",
46
+ "UP",
47
+ ]
48
+ lint.ignore = [
49
+ "UP006",
50
+ "UP007",
51
+ "UP035",
52
+ "D417",
53
+ "E501",
54
+ ]
55
+
56
+ [tool.ruff.lint.per-file-ignores]
57
+ "tests/*" = ["D", "UP"]
58
+
59
+ [tool.ruff.lint.pydocstyle]
60
+ convention = "google"
61
+
62
+ [dependency-groups]
63
+ dev = [
64
+ "ruff>=0.12.7",
65
+ ]
backend/src/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LangGraph Deep Research - A deep research assistant powered by LangGraph."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .agent import DeepResearchAgent
6
+ from .config import Configuration, SearchAPI
7
+ from .models import SummaryState, SummaryStateInput, SummaryStateOutput, TodoItem
8
+
9
+ __all__ = [
10
+ "DeepResearchAgent",
11
+ "Configuration",
12
+ "SearchAPI",
13
+ "SummaryState",
14
+ "SummaryStateInput",
15
+ "SummaryStateOutput",
16
+ "TodoItem",
17
+ ]
18
+
backend/src/agent.py ADDED
@@ -0,0 +1,919 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Orchestrator coordinating the deep research workflow using LangGraph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ import operator
8
+ from pathlib import Path
9
+ from queue import Empty, Queue
10
+ from threading import Lock, Thread
11
+ from typing import Any, Annotated, Iterator, TypedDict, Optional, Callable
12
+
13
+ from langchain_openai import ChatOpenAI
14
+ from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
15
+ from langchain_core.tools import tool
16
+ from langgraph.graph import StateGraph, END
17
+
18
+ from config import Configuration
19
+ from prompts import (
20
+ report_writer_instructions,
21
+ task_summarizer_instructions,
22
+ todo_planner_system_prompt,
23
+ todo_planner_instructions,
24
+ get_current_date,
25
+ )
26
+ from models import SummaryState, SummaryStateOutput, TodoItem
27
+ from services.search import dispatch_search, prepare_research_context
28
+ from utils import strip_thinking_tokens
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ # ============================================================================
34
+ # State Schema
35
+ # ============================================================================
36
+ class ResearchState(TypedDict, total=False):
37
+ """State schema for the research workflow graph."""
38
+ research_topic: str
39
+ todo_items: list[TodoItem]
40
+ current_task_index: int
41
+ web_research_results: Annotated[list[str], operator.add]
42
+ sources_gathered: Annotated[list[str], operator.add]
43
+ research_loop_count: int
44
+ structured_report: Optional[str]
45
+ report_note_id: Optional[str]
46
+ report_note_path: Optional[str]
47
+ # Internal tracking
48
+ messages: list[Any]
49
+ config: Configuration
50
+
51
+
52
+ # ============================================================================
53
+ # Note Tool Implementation
54
+ # ============================================================================
55
+ class NoteTool:
56
+ """Simple file-based note tool for persisting task notes."""
57
+
58
+ def __init__(self, workspace: str = "./notes"):
59
+ self.workspace = Path(workspace)
60
+ self.workspace.mkdir(parents=True, exist_ok=True)
61
+ self._id_counter = 0
62
+ self._lock = Lock()
63
+
64
+ def _generate_id(self) -> str:
65
+ with self._lock:
66
+ self._id_counter += 1
67
+ import time
68
+ return f"note_{int(time.time())}_{self._id_counter}"
69
+
70
+ def run(self, params: dict[str, Any]) -> str:
71
+ """Execute note action: create, read, update, list."""
72
+ action = params.get("action", "read")
73
+
74
+ if action == "create":
75
+ return self._create_note(params)
76
+ elif action == "read":
77
+ return self._read_note(params)
78
+ elif action == "update":
79
+ return self._update_note(params)
80
+ elif action == "list":
81
+ return self._list_notes(params)
82
+ else:
83
+ return f"❌ Unknown action: {action}"
84
+
85
+ def _create_note(self, params: dict[str, Any]) -> str:
86
+ note_id = self._generate_id()
87
+ title = params.get("title", "Untitled")
88
+ note_type = params.get("note_type", "general")
89
+ tags = params.get("tags", [])
90
+ content = params.get("content", "")
91
+ task_id = params.get("task_id")
92
+
93
+ note_path = self.workspace / f"{note_id}.md"
94
+
95
+ frontmatter = f"""---
96
+ id: {note_id}
97
+ title: {title}
98
+ type: {note_type}
99
+ tags: {tags}
100
+ task_id: {task_id}
101
+ ---
102
+
103
+ """
104
+ note_path.write_text(frontmatter + content, encoding="utf-8")
105
+ return f"✅ Note created\nID: {note_id}\nPath: {note_path}"
106
+
107
+ def _read_note(self, params: dict[str, Any]) -> str:
108
+ note_id = params.get("note_id")
109
+ if not note_id:
110
+ return "❌ Missing note_id parameter"
111
+
112
+ note_path = self.workspace / f"{note_id}.md"
113
+ if not note_path.exists():
114
+ return f"❌ Note does not exist: {note_id}"
115
+
116
+ content = note_path.read_text(encoding="utf-8")
117
+ return f"✅ Note content:\n{content}"
118
+
119
+ def _update_note(self, params: dict[str, Any]) -> str:
120
+ note_id = params.get("note_id")
121
+ if not note_id:
122
+ return "❌ Missing note_id parameter"
123
+
124
+ note_path = self.workspace / f"{note_id}.md"
125
+ if not note_path.exists():
126
+ return f"❌ Note does not exist: {note_id}"
127
+
128
+ # Read existing content
129
+ existing = note_path.read_text(encoding="utf-8")
130
+
131
+ # Update frontmatter if provided
132
+ title = params.get("title")
133
+ content = params.get("content", "")
134
+
135
+ # Simple append strategy
136
+ if content:
137
+ updated = existing + "\n\n---\nUpdate:\n" + content
138
+ note_path.write_text(updated, encoding="utf-8")
139
+
140
+ return f"✅ Note updated\nID: {note_id}"
141
+
142
+ def _list_notes(self, params: dict[str, Any]) -> str:
143
+ notes = list(self.workspace.glob("*.md"))
144
+ if not notes:
145
+ return "📝 No notes yet"
146
+
147
+ result = "📝 Note list:\n"
148
+ for note in notes:
149
+ result += f"- {note.stem}\n"
150
+ return result
151
+
152
+
153
+ # ============================================================================
154
+ # Tool Call Tracker
155
+ # ============================================================================
156
+ class ToolCallTracker:
157
+ """Collects tool call events for SSE streaming."""
158
+
159
+ def __init__(self, notes_workspace: Optional[str] = None):
160
+ self._notes_workspace = notes_workspace
161
+ self._events: list[dict[str, Any]] = []
162
+ self._cursor = 0
163
+ self._lock = Lock()
164
+ self._event_sink: Optional[Callable[[dict[str, Any]], None]] = None
165
+
166
+ def record(self, event: dict[str, Any]) -> None:
167
+ with self._lock:
168
+ event["id"] = len(self._events) + 1
169
+ self._events.append(event)
170
+
171
+ sink = self._event_sink
172
+ if sink:
173
+ sink({"type": "tool_call", **event})
174
+
175
+ def drain(self, step: Optional[int] = None) -> list[dict[str, Any]]:
176
+ with self._lock:
177
+ if self._cursor >= len(self._events):
178
+ return []
179
+ new_events = self._events[self._cursor:]
180
+ self._cursor = len(self._events)
181
+
182
+ payloads = []
183
+ for event in new_events:
184
+ payload = {"type": "tool_call", **event}
185
+ if step is not None:
186
+ payload["step"] = step
187
+ payloads.append(payload)
188
+ return payloads
189
+
190
+ def set_event_sink(self, sink: Optional[Callable[[dict[str, Any]], None]]) -> None:
191
+ self._event_sink = sink
192
+
193
+ def as_dicts(self) -> list[dict[str, Any]]:
194
+ with self._lock:
195
+ return list(self._events)
196
+
197
+ def reset(self) -> None:
198
+ with self._lock:
199
+ self._events.clear()
200
+ self._cursor = 0
201
+
202
+
203
+ # ============================================================================
204
+ # Deep Research Agent using LangGraph
205
+ # ============================================================================
206
+ class DeepResearchAgent:
207
+ """Coordinator orchestrating TODO-based research workflow using LangGraph."""
208
+
209
+ def __init__(self, config: Configuration | None = None) -> None:
210
+ """Initialize the coordinator with configuration and LangGraph components."""
211
+ self.config = config or Configuration.from_env()
212
+ self.llm = self._init_llm()
213
+
214
+ # Note tool setup
215
+ self.note_tool = (
216
+ NoteTool(workspace=self.config.notes_workspace)
217
+ if self.config.enable_notes
218
+ else None
219
+ )
220
+
221
+ # Tool call tracking
222
+ self._tool_tracker = ToolCallTracker(
223
+ self.config.notes_workspace if self.config.enable_notes else None
224
+ )
225
+ self._tool_event_sink_enabled = False
226
+ self._state_lock = Lock()
227
+
228
+ # Build the graph
229
+ self.graph = self._build_graph()
230
+ self._last_search_notices: list[str] = []
231
+
232
+ def _init_llm(self) -> ChatOpenAI:
233
+ """Initialize ChatOpenAI with configuration preferences."""
234
+ llm_kwargs: dict[str, Any] = {
235
+ "temperature": 0.0,
236
+ "streaming": True,
237
+ }
238
+
239
+ model_id = self.config.llm_model_id or self.config.local_llm
240
+ if model_id:
241
+ llm_kwargs["model"] = model_id
242
+
243
+ provider = (self.config.llm_provider or "").strip()
244
+
245
+ if provider == "ollama":
246
+ llm_kwargs["base_url"] = self.config.sanitized_ollama_url()
247
+ llm_kwargs["api_key"] = self.config.llm_api_key or "ollama"
248
+ elif provider == "lmstudio":
249
+ llm_kwargs["base_url"] = self.config.lmstudio_base_url
250
+ if self.config.llm_api_key:
251
+ llm_kwargs["api_key"] = self.config.llm_api_key
252
+ else:
253
+ llm_kwargs["api_key"] = "lm-studio"
254
+ else:
255
+ if self.config.llm_base_url:
256
+ llm_kwargs["base_url"] = self.config.llm_base_url
257
+ if self.config.llm_api_key:
258
+ llm_kwargs["api_key"] = self.config.llm_api_key
259
+
260
+ return ChatOpenAI(**llm_kwargs)
261
+
262
+ def _build_graph(self) -> StateGraph:
263
+ """Build the LangGraph workflow."""
264
+ workflow = StateGraph(ResearchState)
265
+
266
+ # Add nodes
267
+ workflow.add_node("plan_research", self._plan_research_node)
268
+ workflow.add_node("execute_tasks", self._execute_tasks_node)
269
+ workflow.add_node("generate_report", self._generate_report_node)
270
+
271
+ # Define edges
272
+ workflow.set_entry_point("plan_research")
273
+ workflow.add_edge("plan_research", "execute_tasks")
274
+ workflow.add_edge("execute_tasks", "generate_report")
275
+ workflow.add_edge("generate_report", END)
276
+
277
+ return workflow.compile()
278
+
279
+ # -------------------------------------------------------------------------
280
+ # Graph Nodes
281
+ # -------------------------------------------------------------------------
282
+ def _plan_research_node(self, state: ResearchState) -> dict[str, Any]:
283
+ """Planning node: break research topic into actionable tasks."""
284
+ topic = state.get("research_topic", "")
285
+
286
+ system_prompt = todo_planner_system_prompt.strip()
287
+ user_prompt = todo_planner_instructions.format(
288
+ current_date=get_current_date(),
289
+ research_topic=topic,
290
+ )
291
+
292
+ messages = [
293
+ SystemMessage(content=system_prompt),
294
+ HumanMessage(content=user_prompt),
295
+ ]
296
+
297
+ response = self.llm.invoke(messages)
298
+ response_text = response.content
299
+
300
+ if self.config.strip_thinking_tokens:
301
+ response_text = strip_thinking_tokens(response_text)
302
+
303
+ logger.info("Planner raw output (truncated): %s", response_text[:500])
304
+
305
+ # Parse tasks from response
306
+ todo_items = self._parse_todo_items(response_text, topic)
307
+
308
+ # Create notes for each task if enabled
309
+ if self.note_tool:
310
+ for task in todo_items:
311
+ result = self.note_tool.run({
312
+ "action": "create",
313
+ "task_id": task.id,
314
+ "title": f"Task {task.id}: {task.title}",
315
+ "note_type": "task_state",
316
+ "tags": ["deep_research", f"task_{task.id}"],
317
+ "content": f"Task objective: {task.intent}\nSearch query: {task.query}",
318
+ })
319
+ # Extract note_id from result
320
+ note_id = self._extract_note_id(result)
321
+ if note_id:
322
+ task.note_id = note_id
323
+ task.note_path = str(Path(self.config.notes_workspace) / f"{note_id}.md")
324
+
325
+ self._tool_tracker.record({
326
+ "agent": "Research Planning Expert",
327
+ "tool": "note",
328
+ "parameters": {"action": "create", "task_id": task.id},
329
+ "result": result,
330
+ "task_id": task.id,
331
+ "note_id": note_id,
332
+ })
333
+
334
+ titles = [task.title for task in todo_items]
335
+ logger.info("Planner produced %d tasks: %s", len(todo_items), titles)
336
+
337
+ return {
338
+ "todo_items": todo_items,
339
+ "current_task_index": 0,
340
+ "research_loop_count": 0,
341
+ }
342
+
343
+ def _execute_tasks_node(self, state: ResearchState) -> dict[str, Any]:
344
+ """Execute research tasks: search and summarize each task."""
345
+ todo_items = state.get("todo_items", [])
346
+ topic = state.get("research_topic", "")
347
+ loop_count = state.get("research_loop_count", 0)
348
+
349
+ web_results: list[str] = []
350
+ sources: list[str] = []
351
+
352
+ for task in todo_items:
353
+ task.status = "in_progress"
354
+
355
+ # Execute search
356
+ search_result, notices, answer_text, backend = dispatch_search(
357
+ task.query,
358
+ self.config,
359
+ loop_count,
360
+ )
361
+ self._last_search_notices = notices
362
+ task.notices = notices
363
+
364
+ if not search_result or not search_result.get("results"):
365
+ task.status = "skipped"
366
+ continue
367
+
368
+ # Prepare context
369
+ sources_summary, context = prepare_research_context(
370
+ search_result, answer_text, self.config
371
+ )
372
+ task.sources_summary = sources_summary
373
+ web_results.append(context)
374
+ sources.append(sources_summary)
375
+
376
+ # Summarize task
377
+ summary = self._summarize_task(topic, task, context)
378
+ task.summary = summary
379
+ task.status = "completed"
380
+
381
+ # Update note if enabled
382
+ if self.note_tool and task.note_id:
383
+ result = self.note_tool.run({
384
+ "action": "update",
385
+ "note_id": task.note_id,
386
+ "task_id": task.id,
387
+ "content": f"## Task Summary\n{summary}\n\n## Sources\n{sources_summary}",
388
+ })
389
+ self._tool_tracker.record({
390
+ "agent": "Task Summary Expert",
391
+ "tool": "note",
392
+ "parameters": {"action": "update", "note_id": task.note_id},
393
+ "result": result,
394
+ "task_id": task.id,
395
+ "note_id": task.note_id,
396
+ })
397
+
398
+ loop_count += 1
399
+
400
+ return {
401
+ "todo_items": todo_items,
402
+ "web_research_results": web_results,
403
+ "sources_gathered": sources,
404
+ "research_loop_count": loop_count,
405
+ }
406
+
407
+ def _generate_report_node(self, state: ResearchState) -> dict[str, Any]:
408
+ """Generate the final structured report."""
409
+ topic = state.get("research_topic", "")
410
+ todo_items = state.get("todo_items", [])
411
+
412
+ # Build task overview
413
+ tasks_block = []
414
+ for task in todo_items:
415
+ summary_block = task.summary or "No information available"
416
+ sources_block = task.sources_summary or "No sources available"
417
+ tasks_block.append(
418
+ f"### Task {task.id}: {task.title}\n"
419
+ f"- Objective: {task.intent}\n"
420
+ f"- Search query: {task.query}\n"
421
+ f"- Status: {task.status}\n"
422
+ f"- Summary:\n{summary_block}\n"
423
+ f"- Sources:\n{sources_block}\n"
424
+ )
425
+
426
+ prompt = (
427
+ f"Research topic: {topic}\n"
428
+ f"Task overview:\n{''.join(tasks_block)}\n"
429
+ "Based on the above task summaries, please write a structured research report."
430
+ )
431
+
432
+ messages = [
433
+ SystemMessage(content=report_writer_instructions.strip()),
434
+ HumanMessage(content=prompt),
435
+ ]
436
+
437
+ response = self.llm.invoke(messages)
438
+ report_text = response.content
439
+
440
+ if self.config.strip_thinking_tokens:
441
+ report_text = strip_thinking_tokens(report_text)
442
+
443
+ report_text = report_text.strip() or "Report generation failed, please check input."
444
+
445
+ # Create conclusion note if enabled
446
+ report_note_id = None
447
+ report_note_path = None
448
+ if self.note_tool and report_text:
449
+ result = self.note_tool.run({
450
+ "action": "create",
451
+ "title": f"Research Report: {topic}",
452
+ "note_type": "conclusion",
453
+ "tags": ["deep_research", "report"],
454
+ "content": report_text,
455
+ })
456
+ report_note_id = self._extract_note_id(result)
457
+ if report_note_id:
458
+ report_note_path = str(Path(self.config.notes_workspace) / f"{report_note_id}.md")
459
+
460
+ self._tool_tracker.record({
461
+ "agent": "Report Writing Expert",
462
+ "tool": "note",
463
+ "parameters": {"action": "create", "note_type": "conclusion"},
464
+ "result": result,
465
+ "note_id": report_note_id,
466
+ })
467
+
468
+ return {
469
+ "structured_report": report_text,
470
+ "report_note_id": report_note_id,
471
+ "report_note_path": report_note_path,
472
+ }
473
+
474
+ # -------------------------------------------------------------------------
475
+ # Helper Methods
476
+ # -------------------------------------------------------------------------
477
+ def _summarize_task(self, topic: str, task: TodoItem, context: str) -> str:
478
+ """Generate summary for a single task."""
479
+ prompt = (
480
+ f"Task topic: {topic}\n"
481
+ f"Task name: {task.title}\n"
482
+ f"Task objective: {task.intent}\n"
483
+ f"Search query: {task.query}\n"
484
+ f"Task context:\n{context}\n"
485
+ "Please generate a detailed task summary."
486
+ )
487
+
488
+ messages = [
489
+ SystemMessage(content=task_summarizer_instructions.strip()),
490
+ HumanMessage(content=prompt),
491
+ ]
492
+
493
+ response = self.llm.invoke(messages)
494
+ summary_text = response.content
495
+
496
+ if self.config.strip_thinking_tokens:
497
+ summary_text = strip_thinking_tokens(summary_text)
498
+
499
+ return summary_text.strip() or "No information available"
500
+
501
+ def _parse_todo_items(self, response: str, topic: str) -> list[TodoItem]:
502
+ """Parse planner output into TodoItem list."""
503
+ import json
504
+
505
+ text = response.strip()
506
+ tasks_payload: list[dict[str, Any]] = []
507
+
508
+ # Try to extract JSON
509
+ start = text.find("{")
510
+ end = text.rfind("}")
511
+ if start != -1 and end != -1 and end > start:
512
+ try:
513
+ json_obj = json.loads(text[start:end + 1])
514
+ if isinstance(json_obj, dict) and "tasks" in json_obj:
515
+ tasks_payload = json_obj["tasks"]
516
+ except json.JSONDecodeError:
517
+ pass
518
+
519
+ if not tasks_payload:
520
+ start = text.find("[")
521
+ end = text.rfind("]")
522
+ if start != -1 and end != -1 and end > start:
523
+ try:
524
+ tasks_payload = json.loads(text[start:end + 1])
525
+ except json.JSONDecodeError:
526
+ pass
527
+
528
+ # Create TodoItems
529
+ todo_items: list[TodoItem] = []
530
+ for idx, item in enumerate(tasks_payload, start=1):
531
+ if not isinstance(item, dict):
532
+ continue
533
+ title = str(item.get("title") or f"Task{idx}").strip()
534
+ intent = str(item.get("intent") or "Focus on key issues of the topic").strip()
535
+ query = str(item.get("query") or topic).strip() or topic
536
+
537
+ todo_items.append(TodoItem(
538
+ id=idx,
539
+ title=title,
540
+ intent=intent,
541
+ query=query,
542
+ ))
543
+
544
+ # Fallback if no tasks parsed
545
+ if not todo_items:
546
+ todo_items.append(TodoItem(
547
+ id=1,
548
+ title="Basic Background Overview",
549
+ intent="Collect core background and latest developments on the topic",
550
+ query=f"{topic} latest developments" if topic else "Basic background overview",
551
+ ))
552
+
553
+ return todo_items
554
+
555
+ @staticmethod
556
+ def _extract_note_id(response: str) -> Optional[str]:
557
+ """Extract note ID from tool response."""
558
+ if not response:
559
+ return None
560
+ match = re.search(r"ID:\s*([^\n]+)", response)
561
+ return match.group(1).strip() if match else None
562
+
563
+ def _set_tool_event_sink(self, sink: Callable[[dict[str, Any]], None] | None) -> None:
564
+ """Enable or disable immediate tool event callbacks."""
565
+ self._tool_event_sink_enabled = sink is not None
566
+ self._tool_tracker.set_event_sink(sink)
567
+
568
+ # -------------------------------------------------------------------------
569
+ # Public API
570
+ # -------------------------------------------------------------------------
571
+ def run(self, topic: str) -> SummaryStateOutput:
572
+ """Execute the research workflow and return the final report."""
573
+ initial_state: ResearchState = {
574
+ "research_topic": topic,
575
+ "todo_items": [],
576
+ "current_task_index": 0,
577
+ "web_research_results": [],
578
+ "sources_gathered": [],
579
+ "research_loop_count": 0,
580
+ "structured_report": None,
581
+ "report_note_id": None,
582
+ "report_note_path": None,
583
+ "messages": [],
584
+ "config": self.config,
585
+ }
586
+
587
+ # Run the graph
588
+ final_state = self.graph.invoke(initial_state)
589
+
590
+ report = final_state.get("structured_report", "")
591
+ todo_items = final_state.get("todo_items", [])
592
+
593
+ return SummaryStateOutput(
594
+ running_summary=report,
595
+ report_markdown=report,
596
+ todo_items=todo_items,
597
+ )
598
+
599
+ def run_stream(self, topic: str) -> Iterator[dict[str, Any]]:
600
+ """Execute the workflow yielding incremental progress events."""
601
+ logger.debug("Starting streaming research: topic=%s", topic)
602
+ yield {"type": "status", "message": "Initializing research workflow"}
603
+
604
+ # Plan phase
605
+ yield {"type": "status", "message": "Planning research tasks..."}
606
+
607
+ system_prompt = todo_planner_system_prompt.strip()
608
+ user_prompt = todo_planner_instructions.format(
609
+ current_date=get_current_date(),
610
+ research_topic=topic,
611
+ )
612
+
613
+ messages = [
614
+ SystemMessage(content=system_prompt),
615
+ HumanMessage(content=user_prompt),
616
+ ]
617
+
618
+ response = self.llm.invoke(messages)
619
+ response_text = response.content
620
+
621
+ if self.config.strip_thinking_tokens:
622
+ response_text = strip_thinking_tokens(response_text)
623
+
624
+ todo_items = self._parse_todo_items(response_text, topic)
625
+
626
+ # Create notes for tasks
627
+ if self.note_tool:
628
+ for task in todo_items:
629
+ result = self.note_tool.run({
630
+ "action": "create",
631
+ "task_id": task.id,
632
+ "title": f"Task {task.id}: {task.title}",
633
+ "note_type": "task_state",
634
+ "tags": ["deep_research", f"task_{task.id}"],
635
+ "content": f"Task objective: {task.intent}\nSearch query: {task.query}",
636
+ })
637
+ note_id = self._extract_note_id(result)
638
+ if note_id:
639
+ task.note_id = note_id
640
+ task.note_path = str(Path(self.config.notes_workspace) / f"{note_id}.md")
641
+
642
+ # Setup channel mapping for streaming
643
+ channel_map: dict[int, dict[str, Any]] = {}
644
+ for index, task in enumerate(todo_items, start=1):
645
+ token = f"task_{task.id}"
646
+ task.stream_token = token
647
+ channel_map[task.id] = {"step": index, "token": token}
648
+
649
+ yield {
650
+ "type": "todo_list",
651
+ "tasks": [self._serialize_task(t) for t in todo_items],
652
+ "step": 0,
653
+ }
654
+
655
+ # Execute tasks with streaming
656
+ event_queue: Queue[dict[str, Any]] = Queue()
657
+
658
+ def enqueue(event: dict[str, Any], task: Optional[TodoItem] = None, step_override: Optional[int] = None) -> None:
659
+ payload = dict(event)
660
+ target_task_id = payload.get("task_id")
661
+ if task is not None:
662
+ target_task_id = task.id
663
+ payload["task_id"] = task.id
664
+
665
+ channel = channel_map.get(target_task_id) if target_task_id else None
666
+ if channel:
667
+ payload.setdefault("step", channel["step"])
668
+ payload["stream_token"] = channel["token"]
669
+ if step_override is not None:
670
+ payload["step"] = step_override
671
+ event_queue.put(payload)
672
+
673
+ def tool_event_sink(event: dict[str, Any]) -> None:
674
+ enqueue(event)
675
+
676
+ self._set_tool_event_sink(tool_event_sink)
677
+
678
+ threads: list[Thread] = []
679
+ state = SummaryState(research_topic=topic)
680
+ state.todo_items = todo_items
681
+
682
+ def worker(task: TodoItem, step: int) -> None:
683
+ try:
684
+ enqueue({
685
+ "type": "task_status",
686
+ "task_id": task.id,
687
+ "status": "in_progress",
688
+ "title": task.title,
689
+ "intent": task.intent,
690
+ "note_id": task.note_id,
691
+ "note_path": task.note_path,
692
+ }, task=task)
693
+
694
+ # Execute search
695
+ search_result, notices, answer_text, backend = dispatch_search(
696
+ task.query, self.config, state.research_loop_count
697
+ )
698
+ task.notices = notices
699
+
700
+ for notice in notices:
701
+ if notice:
702
+ enqueue({
703
+ "type": "status",
704
+ "message": notice,
705
+ "task_id": task.id,
706
+ }, task=task)
707
+
708
+ if not search_result or not search_result.get("results"):
709
+ task.status = "skipped"
710
+ enqueue({
711
+ "type": "task_status",
712
+ "task_id": task.id,
713
+ "status": "skipped",
714
+ "title": task.title,
715
+ "intent": task.intent,
716
+ "note_id": task.note_id,
717
+ "note_path": task.note_path,
718
+ }, task=task)
719
+ return
720
+
721
+ # Prepare context
722
+ sources_summary, context = prepare_research_context(
723
+ search_result, answer_text, self.config
724
+ )
725
+ task.sources_summary = sources_summary
726
+
727
+ with self._state_lock:
728
+ state.web_research_results.append(context)
729
+ state.sources_gathered.append(sources_summary)
730
+ state.research_loop_count += 1
731
+
732
+ enqueue({
733
+ "type": "sources",
734
+ "task_id": task.id,
735
+ "latest_sources": sources_summary,
736
+ "raw_context": context,
737
+ "backend": backend,
738
+ "note_id": task.note_id,
739
+ "note_path": task.note_path,
740
+ }, task=task)
741
+
742
+ # Stream summarization
743
+ prompt = (
744
+ f"Task topic: {topic}\n"
745
+ f"Task name: {task.title}\n"
746
+ f"Task objective: {task.intent}\n"
747
+ f"Search query: {task.query}\n"
748
+ f"Task context:\n{context}\n"
749
+ "Please generate a detailed task summary."
750
+ )
751
+
752
+ summary_messages = [
753
+ SystemMessage(content=task_summarizer_instructions.strip()),
754
+ HumanMessage(content=prompt),
755
+ ]
756
+
757
+ summary_chunks: list[str] = []
758
+ for chunk in self.llm.stream(summary_messages):
759
+ chunk_text = chunk.content
760
+ if chunk_text:
761
+ summary_chunks.append(chunk_text)
762
+ # Strip thinking tokens from visible output
763
+ visible_chunk = chunk_text
764
+ if self.config.strip_thinking_tokens and "<think>" not in chunk_text:
765
+ enqueue({
766
+ "type": "task_summary_chunk",
767
+ "task_id": task.id,
768
+ "content": visible_chunk,
769
+ "note_id": task.note_id,
770
+ }, task=task)
771
+
772
+ full_summary = "".join(summary_chunks)
773
+ if self.config.strip_thinking_tokens:
774
+ full_summary = strip_thinking_tokens(full_summary)
775
+
776
+ task.summary = full_summary.strip() or "No information available"
777
+ task.status = "completed"
778
+
779
+ # Update note
780
+ if self.note_tool and task.note_id:
781
+ self.note_tool.run({
782
+ "action": "update",
783
+ "note_id": task.note_id,
784
+ "task_id": task.id,
785
+ "content": f"## Task Summary\n{task.summary}\n\n## Sources\n{sources_summary}",
786
+ })
787
+
788
+ enqueue({
789
+ "type": "task_status",
790
+ "task_id": task.id,
791
+ "status": "completed",
792
+ "summary": task.summary,
793
+ "sources_summary": task.sources_summary,
794
+ "note_id": task.note_id,
795
+ "note_path": task.note_path,
796
+ }, task=task)
797
+
798
+ except Exception as exc:
799
+ logger.exception("Task execution failed", exc_info=exc)
800
+ enqueue({
801
+ "type": "task_status",
802
+ "task_id": task.id,
803
+ "status": "failed",
804
+ "detail": str(exc),
805
+ "title": task.title,
806
+ "intent": task.intent,
807
+ "note_id": task.note_id,
808
+ "note_path": task.note_path,
809
+ }, task=task)
810
+ finally:
811
+ enqueue({"type": "__task_done__", "task_id": task.id})
812
+
813
+ # Start worker threads
814
+ for task in todo_items:
815
+ step = channel_map.get(task.id, {}).get("step", 0)
816
+ thread = Thread(target=worker, args=(task, step), daemon=True)
817
+ threads.append(thread)
818
+ thread.start()
819
+
820
+ # Yield events from queue
821
+ active_workers = len(todo_items)
822
+ finished_workers = 0
823
+
824
+ try:
825
+ while finished_workers < active_workers:
826
+ event = event_queue.get()
827
+ if event.get("type") == "__task_done__":
828
+ finished_workers += 1
829
+ continue
830
+ yield event
831
+
832
+ # Drain remaining events
833
+ while True:
834
+ try:
835
+ event = event_queue.get_nowait()
836
+ except Empty:
837
+ break
838
+ if event.get("type") != "__task_done__":
839
+ yield event
840
+ finally:
841
+ self._set_tool_event_sink(None)
842
+ for thread in threads:
843
+ thread.join()
844
+
845
+ # Generate final report
846
+ yield {"type": "status", "message": "Generating research report..."}
847
+
848
+ tasks_block = []
849
+ for task in todo_items:
850
+ summary_block = task.summary or "No information available"
851
+ sources_block = task.sources_summary or "No sources available"
852
+ tasks_block.append(
853
+ f"### Task {task.id}: {task.title}\n"
854
+ f"- Objective: {task.intent}\n"
855
+ f"- Search query: {task.query}\n"
856
+ f"- Status: {task.status}\n"
857
+ f"- Summary:\n{summary_block}\n"
858
+ f"- Sources:\n{sources_block}\n"
859
+ )
860
+
861
+ report_prompt = (
862
+ f"Research topic: {topic}\n"
863
+ f"Task overview:\n{''.join(tasks_block)}\n"
864
+ "Based on the above task summaries, please write a structured research report."
865
+ )
866
+
867
+ report_messages = [
868
+ SystemMessage(content=report_writer_instructions.strip()),
869
+ HumanMessage(content=report_prompt),
870
+ ]
871
+
872
+ report = self.llm.invoke(report_messages).content
873
+ if self.config.strip_thinking_tokens:
874
+ report = strip_thinking_tokens(report)
875
+ report = report.strip() or "Report generation failed"
876
+
877
+ # Create conclusion note
878
+ report_note_id = None
879
+ report_note_path = None
880
+ if self.note_tool:
881
+ result = self.note_tool.run({
882
+ "action": "create",
883
+ "title": f"Research Report: {topic}",
884
+ "note_type": "conclusion",
885
+ "tags": ["deep_research", "report"],
886
+ "content": report,
887
+ })
888
+ report_note_id = self._extract_note_id(result)
889
+ if report_note_id:
890
+ report_note_path = str(Path(self.config.notes_workspace) / f"{report_note_id}.md")
891
+
892
+ yield {
893
+ "type": "final_report",
894
+ "report": report,
895
+ "note_id": report_note_id,
896
+ "note_path": report_note_path,
897
+ }
898
+ yield {"type": "done"}
899
+
900
+ def _serialize_task(self, task: TodoItem) -> dict[str, Any]:
901
+ """Convert task dataclass to serializable dict for frontend."""
902
+ return {
903
+ "id": task.id,
904
+ "title": task.title,
905
+ "intent": task.intent,
906
+ "query": task.query,
907
+ "status": task.status,
908
+ "summary": task.summary,
909
+ "sources_summary": task.sources_summary,
910
+ "note_id": task.note_id,
911
+ "note_path": task.note_path,
912
+ "stream_token": task.stream_token,
913
+ }
914
+
915
+
916
+ def run_deep_research(topic: str, config: Configuration | None = None) -> SummaryStateOutput:
917
+ """Convenience function mirroring the class-based API."""
918
+ agent = DeepResearchAgent(config=config)
919
+ return agent.run(topic)
backend/src/config.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from enum import Enum
3
+ from typing import Any, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SearchAPI(Enum):
9
+ PERPLEXITY = "perplexity"
10
+ TAVILY = "tavily"
11
+ DUCKDUCKGO = "duckduckgo"
12
+ SEARXNG = "searxng"
13
+ ADVANCED = "advanced"
14
+
15
+
16
+ class Configuration(BaseModel):
17
+ """Configuration options for the deep research assistant."""
18
+
19
+ max_web_research_loops: int = Field(
20
+ default=3,
21
+ title="Research Depth",
22
+ description="Number of research iterations to perform",
23
+ )
24
+ local_llm: str = Field(
25
+ default="llama3.2",
26
+ title="Local Model Name",
27
+ description="Name of the locally hosted LLM (Ollama/LMStudio)",
28
+ )
29
+ llm_provider: str = Field(
30
+ default="ollama",
31
+ title="LLM Provider",
32
+ description="Provider identifier (ollama, lmstudio, or custom)",
33
+ )
34
+ search_api: SearchAPI = Field(
35
+ default=SearchAPI.DUCKDUCKGO,
36
+ title="Search API",
37
+ description="Web search API to use",
38
+ )
39
+ enable_notes: bool = Field(
40
+ default=True,
41
+ title="Enable Notes",
42
+ description="Whether to store task progress in NoteTool",
43
+ )
44
+ notes_workspace: str = Field(
45
+ default="./notes",
46
+ title="Notes Workspace",
47
+ description="Directory for NoteTool to persist task notes",
48
+ )
49
+ fetch_full_page: bool = Field(
50
+ default=True,
51
+ title="Fetch Full Page",
52
+ description="Include the full page content in the search results",
53
+ )
54
+ ollama_base_url: str = Field(
55
+ default="http://localhost:11434",
56
+ title="Ollama Base URL",
57
+ description="Base URL for Ollama API (without /v1 suffix)",
58
+ )
59
+ lmstudio_base_url: str = Field(
60
+ default="http://localhost:1234/v1",
61
+ title="LMStudio Base URL",
62
+ description="Base URL for LMStudio OpenAI-compatible API",
63
+ )
64
+ strip_thinking_tokens: bool = Field(
65
+ default=True,
66
+ title="Strip Thinking Tokens",
67
+ description="Whether to strip <think> tokens from model responses",
68
+ )
69
+ use_tool_calling: bool = Field(
70
+ default=False,
71
+ title="Use Tool Calling",
72
+ description="Use tool calling instead of JSON mode for structured output",
73
+ )
74
+ llm_api_key: Optional[str] = Field(
75
+ default=None,
76
+ title="LLM API Key",
77
+ description="Optional API key when using custom OpenAI-compatible services",
78
+ )
79
+ llm_base_url: Optional[str] = Field(
80
+ default=None,
81
+ title="LLM Base URL",
82
+ description="Optional base URL when using custom OpenAI-compatible services",
83
+ )
84
+ llm_model_id: Optional[str] = Field(
85
+ default=None,
86
+ title="LLM Model ID",
87
+ description="Optional model identifier for custom OpenAI-compatible services",
88
+ )
89
+
90
+ @classmethod
91
+ def from_env(cls, overrides: Optional[dict[str, Any]] = None) -> "Configuration":
92
+ """Create a configuration object using environment variables and overrides."""
93
+
94
+ raw_values: dict[str, Any] = {}
95
+
96
+ # Load values from environment variables based on field names
97
+ for field_name in cls.model_fields.keys():
98
+ env_key = field_name.upper()
99
+ if env_key in os.environ:
100
+ raw_values[field_name] = os.environ[env_key]
101
+
102
+ # Additional mappings for explicit env names
103
+ env_aliases = {
104
+ "local_llm": os.getenv("LOCAL_LLM"),
105
+ "llm_provider": os.getenv("LLM_PROVIDER"),
106
+ "llm_api_key": os.getenv("LLM_API_KEY"),
107
+ "llm_model_id": os.getenv("LLM_MODEL_ID"),
108
+ "llm_base_url": os.getenv("LLM_BASE_URL"),
109
+ "lmstudio_base_url": os.getenv("LMSTUDIO_BASE_URL"),
110
+ "ollama_base_url": os.getenv("OLLAMA_BASE_URL"),
111
+ "max_web_research_loops": os.getenv("MAX_WEB_RESEARCH_LOOPS"),
112
+ "fetch_full_page": os.getenv("FETCH_FULL_PAGE"),
113
+ "strip_thinking_tokens": os.getenv("STRIP_THINKING_TOKENS"),
114
+ "use_tool_calling": os.getenv("USE_TOOL_CALLING"),
115
+ "search_api": os.getenv("SEARCH_API"),
116
+ "enable_notes": os.getenv("ENABLE_NOTES"),
117
+ "notes_workspace": os.getenv("NOTES_WORKSPACE"),
118
+ }
119
+
120
+ for key, value in env_aliases.items():
121
+ if value is not None:
122
+ raw_values.setdefault(key, value)
123
+
124
+ if overrides:
125
+ for key, value in overrides.items():
126
+ if value is not None:
127
+ raw_values[key] = value
128
+
129
+ return cls(**raw_values)
130
+
131
+ def sanitized_ollama_url(self) -> str:
132
+ """Ensure Ollama base URL includes the /v1 suffix required by OpenAI clients."""
133
+
134
+ base = self.ollama_base_url.rstrip("/")
135
+ if not base.endswith("/v1"):
136
+ base = f"{base}/v1"
137
+ return base
138
+
139
+ def resolved_model(self) -> Optional[str]:
140
+ """Best-effort resolution of the model identifier to use."""
141
+
142
+ return self.llm_model_id or self.local_llm
143
+
backend/src/main.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI entrypoint exposing the DeepResearchAgent via HTTP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any, Dict, Iterator, Optional
10
+
11
+ from dotenv import load_dotenv
12
+
13
+ # Load .env file before importing config
14
+ load_dotenv()
15
+
16
+ from fastapi import FastAPI, HTTPException
17
+ from fastapi.middleware.cors import CORSMiddleware
18
+ from fastapi.responses import FileResponse, StreamingResponse
19
+ from fastapi.staticfiles import StaticFiles
20
+ from loguru import logger
21
+ from pydantic import BaseModel, Field
22
+
23
+ from config import Configuration, SearchAPI
24
+ from agent import DeepResearchAgent
25
+
26
+ # Static files directory (for production deployment)
27
+ STATIC_DIR = Path(__file__).parent.parent / "static"
28
+
29
+ # Add console log handler
30
+ logger.add(
31
+ sys.stderr,
32
+ level="INFO",
33
+ format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <4}</level> | <cyan>using_function:{function}</cyan> | <cyan>{file}:{line}</cyan> | <level>{message}</level>",
34
+ colorize=True,
35
+ )
36
+
37
+
38
+ # Add error log handler
39
+ logger.add(
40
+ sink=sys.stderr,
41
+ level="ERROR",
42
+ format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <4}</level> | <cyan>using_function:{function}</cyan> | <cyan>{file}:{line}</cyan> | <level>{message}</level>",
43
+ colorize=True,
44
+ )
45
+
46
+
47
+ class ResearchRequest(BaseModel):
48
+ """Payload for triggering a research run."""
49
+
50
+ topic: str = Field(..., description="Research topic supplied by the user")
51
+ search_api: SearchAPI | None = Field(
52
+ default=None,
53
+ description="Override the default search backend configured via env",
54
+ )
55
+
56
+
57
+ class ResearchResponse(BaseModel):
58
+ """HTTP response containing the generated report and structured tasks."""
59
+
60
+ report_markdown: str = Field(
61
+ ..., description="Markdown-formatted research report including sections"
62
+ )
63
+ todo_items: list[dict[str, Any]] = Field(
64
+ default_factory=list,
65
+ description="Structured TODO items with summaries and sources",
66
+ )
67
+
68
+
69
+ def _mask_secret(value: Optional[str], visible: int = 4) -> str:
70
+ """Mask sensitive tokens while keeping leading and trailing characters."""
71
+ if not value:
72
+ return "unset"
73
+
74
+ if len(value) <= visible * 2:
75
+ return "*" * len(value)
76
+
77
+ return f"{value[:visible]}...{value[-visible:]}"
78
+
79
+
80
+ def _build_config(payload: ResearchRequest) -> Configuration:
81
+ overrides: Dict[str, Any] = {}
82
+
83
+ if payload.search_api is not None:
84
+ overrides["search_api"] = payload.search_api
85
+
86
+ return Configuration.from_env(overrides=overrides)
87
+
88
+
89
+ def create_app() -> FastAPI:
90
+ app = FastAPI(title="LangGraph Deep Researcher")
91
+
92
+ app.add_middleware(
93
+ CORSMiddleware,
94
+ allow_origins=["*"],
95
+ allow_credentials=True,
96
+ allow_methods=["*"],
97
+ allow_headers=["*"],
98
+ )
99
+
100
+ @app.on_event("startup")
101
+ def log_startup_configuration() -> None:
102
+ config = Configuration.from_env()
103
+
104
+ if config.llm_provider == "ollama":
105
+ base_url = config.sanitized_ollama_url()
106
+ elif config.llm_provider == "lmstudio":
107
+ base_url = config.lmstudio_base_url
108
+ else:
109
+ base_url = config.llm_base_url or "unset"
110
+
111
+ logger.info(
112
+ "DeepResearch configuration loaded: provider=%s model=%s base_url=%s search_api=%s "
113
+ "max_loops=%s fetch_full_page=%s tool_calling=%s strip_thinking=%s api_key=%s",
114
+ config.llm_provider,
115
+ config.resolved_model() or "unset",
116
+ base_url,
117
+ (config.search_api.value if isinstance(config.search_api, SearchAPI) else config.search_api),
118
+ config.max_web_research_loops,
119
+ config.fetch_full_page,
120
+ config.use_tool_calling,
121
+ config.strip_thinking_tokens,
122
+ _mask_secret(config.llm_api_key),
123
+ )
124
+
125
+ @app.get("/healthz")
126
+ def health_check() -> Dict[str, str]:
127
+ return {"status": "ok"}
128
+
129
+ @app.post("/research", response_model=ResearchResponse)
130
+ def run_research(payload: ResearchRequest) -> ResearchResponse:
131
+ try:
132
+ config = _build_config(payload)
133
+ agent = DeepResearchAgent(config=config)
134
+ result = agent.run(payload.topic)
135
+ except ValueError as exc: # Likely due to unsupported configuration
136
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
137
+ except Exception as exc: # pragma: no cover - defensive guardrail
138
+ raise HTTPException(status_code=500, detail="Research failed") from exc
139
+
140
+ todo_payload = [
141
+ {
142
+ "id": item.id,
143
+ "title": item.title,
144
+ "intent": item.intent,
145
+ "query": item.query,
146
+ "status": item.status,
147
+ "summary": item.summary,
148
+ "sources_summary": item.sources_summary,
149
+ "note_id": item.note_id,
150
+ "note_path": item.note_path,
151
+ }
152
+ for item in result.todo_items
153
+ ]
154
+
155
+ return ResearchResponse(
156
+ report_markdown=(result.report_markdown or result.running_summary or ""),
157
+ todo_items=todo_payload,
158
+ )
159
+
160
+ @app.post("/research/stream")
161
+ def stream_research(payload: ResearchRequest) -> StreamingResponse:
162
+ try:
163
+ config = _build_config(payload)
164
+ agent = DeepResearchAgent(config=config)
165
+ except ValueError as exc:
166
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
167
+
168
+ def event_iterator() -> Iterator[str]:
169
+ try:
170
+ for event in agent.run_stream(payload.topic):
171
+ yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
172
+ except Exception as exc: # pragma: no cover - defensive guardrail
173
+ logger.exception("Streaming research failed")
174
+ error_payload = {"type": "error", "detail": str(exc)}
175
+ yield f"data: {json.dumps(error_payload, ensure_ascii=False)}\n\n"
176
+
177
+ return StreamingResponse(
178
+ event_iterator(),
179
+ media_type="text/event-stream",
180
+ headers={
181
+ "Cache-Control": "no-cache",
182
+ "Connection": "keep-alive",
183
+ },
184
+ )
185
+
186
+ # Serve static frontend files in production (when static directory exists)
187
+ if STATIC_DIR.exists() and STATIC_DIR.is_dir():
188
+ logger.info(f"Serving static files from {STATIC_DIR}")
189
+
190
+ # Mount assets directory first (CSS, JS, images)
191
+ assets_dir = STATIC_DIR / "assets"
192
+ if assets_dir.exists():
193
+ app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
194
+
195
+ # Serve index.html for root path
196
+ @app.get("/")
197
+ async def serve_index() -> FileResponse:
198
+ """Serve the main index.html."""
199
+ return FileResponse(STATIC_DIR / "index.html")
200
+
201
+ # Serve favicon and other root-level static files
202
+ @app.get("/favicon.ico")
203
+ async def serve_favicon() -> FileResponse:
204
+ """Serve favicon."""
205
+ favicon_path = STATIC_DIR / "favicon.ico"
206
+ if favicon_path.exists():
207
+ return FileResponse(favicon_path)
208
+ raise HTTPException(status_code=404, detail="Favicon not found")
209
+
210
+ # Catch-all for SPA routing (must be last)
211
+ @app.get("/{full_path:path}")
212
+ async def serve_spa(full_path: str) -> FileResponse:
213
+ """Serve the SPA index.html for client-side routing."""
214
+ # Check if requesting a static file that exists
215
+ file_path = STATIC_DIR / full_path
216
+ if file_path.exists() and file_path.is_file():
217
+ return FileResponse(file_path)
218
+ # Otherwise serve index.html for SPA routing
219
+ return FileResponse(STATIC_DIR / "index.html")
220
+
221
+ return app
222
+
223
+
224
+ app = create_app()
225
+
226
+
227
+ if __name__ == "__main__":
228
+ import uvicorn
229
+
230
+ uvicorn.run(
231
+ "main:app",
232
+ host="0.0.0.0",
233
+ port=8000,
234
+ reload=True,
235
+ log_level="info"
236
+ )
backend/src/models.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """State models used by the deep research workflow."""
2
+
3
+ import operator
4
+ from dataclasses import dataclass, field
5
+ from typing import List, Optional
6
+
7
+ from typing_extensions import Annotated
8
+
9
+
10
+ @dataclass(kw_only=True)
11
+ class TodoItem:
12
+ """A single todo task item."""
13
+
14
+ id: int
15
+ title: str
16
+ intent: str
17
+ query: str
18
+ status: str = field(default="pending")
19
+ summary: Optional[str] = field(default=None)
20
+ sources_summary: Optional[str] = field(default=None)
21
+ notices: list[str] = field(default_factory=list)
22
+ note_id: Optional[str] = field(default=None)
23
+ note_path: Optional[str] = field(default=None)
24
+ stream_token: Optional[str] = field(default=None)
25
+
26
+
27
+ @dataclass(kw_only=True)
28
+ class SummaryState:
29
+ research_topic: str = field(default=None) # Report topic
30
+ search_query: str = field(default=None) # Deprecated placeholder
31
+ web_research_results: Annotated[list, operator.add] = field(default_factory=list)
32
+ sources_gathered: Annotated[list, operator.add] = field(default_factory=list)
33
+ research_loop_count: int = field(default=0) # Research loop count
34
+ running_summary: str = field(default=None) # Legacy summary field
35
+ todo_items: Annotated[list, operator.add] = field(default_factory=list)
36
+ structured_report: Optional[str] = field(default=None)
37
+ report_note_id: Optional[str] = field(default=None)
38
+ report_note_path: Optional[str] = field(default=None)
39
+
40
+
41
+ @dataclass(kw_only=True)
42
+ class SummaryStateInput:
43
+ research_topic: str = field(default=None) # Report topic
44
+
45
+
46
+ @dataclass(kw_only=True)
47
+ class SummaryStateOutput:
48
+ running_summary: str = field(default=None) # Backward-compatible text
49
+ report_markdown: Optional[str] = field(default=None)
50
+ todo_items: List[TodoItem] = field(default_factory=list)
51
+
backend/src/prompts.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+
3
+
4
+ # Get current date in a readable format
5
+ def get_current_date():
6
+ return datetime.now().strftime("%B %d, %Y")
7
+
8
+
9
+
10
+ todo_planner_system_prompt = """
11
+ You are a research planning expert. Please break down complex topics into a limited set of complementary tasks.
12
+ - Tasks should be complementary and avoid duplication;
13
+ - Each task should have a clear intent and actionable search direction;
14
+ - Output must be structured, concise, and suitable for subsequent collaboration.
15
+
16
+ <GOAL>
17
+ 1. Based on the research topic, identify 3-5 key research tasks;
18
+ 2. Each task needs a clear objective intent and appropriate web search queries;
19
+ 3. Tasks should avoid overlap and collectively cover the user's problem domain;
20
+ 4. When creating or updating tasks, you must call the `note` tool to sync task information (this is the only way to write to notes).
21
+ </GOAL>
22
+
23
+ <NOTE_COLLAB>
24
+ - Call the `note` tool to create/update structured notes for each task, using JSON parameter format:
25
+ - Create example: `[TOOL_CALL:note:{"action":"create","task_id":1,"title":"Task 1: Background Overview","note_type":"task_state","tags":["deep_research","task_1"],"content":"Please record task overview, system prompts, source overview, task summary"}]`
26
+ - Update example: `[TOOL_CALL:note:{"action":"update","note_id":"<existing_ID>","task_id":1,"title":"Task 1: Background Overview","note_type":"task_state","tags":["deep_research","task_1"],"content":"...new content..."}]`
27
+ - `tags` must include `deep_research` and `task_{task_id}` so other Agents can find them
28
+ </NOTE_COLLAB>
29
+
30
+ <TOOLS>
31
+ You must call the note tool named `note` to record or update tasks, using JSON parameters:
32
+ ```
33
+ [TOOL_CALL:note:{"action":"create","task_id":1,"title":"Task 1: Background Overview","note_type":"task_state","tags":["deep_research","task_1"],"content":"..."}]
34
+ ```
35
+ </TOOLS>
36
+ """
37
+
38
+
39
+ todo_planner_instructions = """
40
+
41
+ <CONTEXT>
42
+ Current date: {current_date}
43
+ Research topic: {research_topic}
44
+ </CONTEXT>
45
+
46
+ <FORMAT>
47
+ Please respond strictly in JSON format:
48
+ {{
49
+ "tasks": [
50
+ {{
51
+ "title": "Task name (within 10 words, highlight key points)",
52
+ "intent": "Core problem the task aims to solve, described in 1-2 sentences",
53
+ "query": "Recommended search keywords"
54
+ }}
55
+ ]
56
+ }}
57
+ </FORMAT>
58
+
59
+ If the topic information is insufficient to plan tasks, output an empty array: {{"tasks": []}}. Use the note tool to record your thought process if necessary.
60
+ """
61
+
62
+
63
+ task_summarizer_instructions = """
64
+ You are a research execution expert. Based on the given context, generate a summary for a specific task. Provide detailed and thorough summaries rather than superficial overviews. Be innovative, break conventional thinking, and expand from multiple dimensions including principles, applications, pros and cons, engineering practices, comparisons, and historical evolution.
65
+
66
+ <GOAL>
67
+ 1. Identify 3-5 key findings related to the task intent;
68
+ 2. Clearly explain the meaning and value of each finding, citing factual data when available;
69
+ </GOAL>
70
+
71
+ <NOTES>
72
+ - Task notes are created by the planning expert. The note ID will be provided at call time; please first call `[TOOL_CALL:note:{"action":"read","note_id":"<note_id>"}]` to get the latest state.
73
+ - After updating the task summary, use `[TOOL_CALL:note:{"action":"update","note_id":"<note_id>","task_id":{task_id},"title":"Task {task_id}: ...","note_type":"task_state","tags":["deep_research","task_{task_id}"],"content":"..."}]` to write back to notes, maintaining the original structure and appending new information.
74
+ - If no note ID is found, please create one first with `task_{task_id}` in `tags` before continuing.
75
+ </NOTES>
76
+
77
+ <FORMAT>
78
+ - Use Markdown for output;
79
+ - Start with section heading: "Task Summary";
80
+ - Express key findings using ordered or unordered lists;
81
+ - If the task has no valid results, output "No information available".
82
+ - The final summary presented to users must NOT contain `[TOOL_CALL:...]` directives.
83
+ </FORMAT>
84
+ """
85
+
86
+
87
+ report_writer_instructions = """
88
+ You are a professional analysis report writer. Based on the input task summaries and reference information, generate a structured research report.
89
+
90
+ <REPORT_TEMPLATE>
91
+ 1. **Background Overview**: Briefly describe the importance and context of the research topic.
92
+ 2. **Core Insights**: Extract 3-5 most important conclusions, annotated with reference/task numbers.
93
+ 3. **Evidence & Data**: List supporting facts or metrics, citing key points from task summaries.
94
+ 4. **Risks & Challenges**: Analyze potential issues, limitations, or hypotheses yet to be verified.
95
+ 5. **Reference Sources**: List key source entries by task (title + link).
96
+ </REPORT_TEMPLATE>
97
+
98
+ <REQUIREMENTS>
99
+ - Report should use Markdown;
100
+ - Each section should be clearly separated, no additional cover page or epilogue;
101
+ - If information is missing for a section, state "No relevant information available";
102
+ - When citing sources, use task titles or source titles to ensure traceability.
103
+ - Output to users must NOT contain residual `[TOOL_CALL:...]` directives.
104
+ </REQUIREMENTS>
105
+
106
+ <NOTES>
107
+ - Before generating the report, call `[TOOL_CALL:note:{"action":"read","note_id":"<note_id>"}]` for each note_id to read task notes.
108
+ - If you need to save results at the report level, you can create a new `conclusion` type note, for example: `[TOOL_CALL:note:{"action":"create","title":"Research Report: {research_topic}","note_type":"conclusion","tags":["deep_research","report"],"content":"...report highlights..."}]`.
109
+ </NOTES>
110
+ """
backend/src/services/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Domain services for the deep researcher workflow."""
2
+
backend/src/services/notes.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helpers for coordinating note tool usage instructions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from models import TodoItem
8
+
9
+
10
+ def build_note_guidance(task: TodoItem) -> str:
11
+ """Generate note tool usage guidance for a specific task."""
12
+
13
+ tags_list = ["deep_research", f"task_{task.id}"]
14
+ tags_literal = json.dumps(tags_list, ensure_ascii=False)
15
+
16
+ if task.note_id:
17
+ read_payload = json.dumps({"action": "read", "note_id": task.note_id}, ensure_ascii=False)
18
+ update_payload = json.dumps(
19
+ {
20
+ "action": "update",
21
+ "note_id": task.note_id,
22
+ "task_id": task.id,
23
+ "title": f"Task {task.id}: {task.title}",
24
+ "note_type": "task_state",
25
+ "tags": tags_list,
26
+ "content": "Please add the new information from this round to the task overview",
27
+ },
28
+ ensure_ascii=False,
29
+ )
30
+
31
+ return (
32
+ "Note collaboration guide:\n"
33
+ f"- Current task note ID: {task.note_id}.\n"
34
+ f"- Before writing the summary, you must call: [TOOL_CALL:note:{read_payload}] to get the latest content.\n"
35
+ f"- After completing analysis, call: [TOOL_CALL:note:{update_payload}] to sync incremental information.\n"
36
+ "- When updating, maintain the original paragraph structure and add new content to the corresponding sections.\n"
37
+ f"- Recommended to keep tags as {tags_literal} so other Agents can quickly locate it.\n"
38
+ "- After successfully syncing to notes, then output the summary for the user.\n"
39
+ )
40
+
41
+ create_payload = json.dumps(
42
+ {
43
+ "action": "create",
44
+ "task_id": task.id,
45
+ "title": f"Task {task.id}: {task.title}",
46
+ "note_type": "task_state",
47
+ "tags": tags_list,
48
+ "content": "Please record task overview, sources overview",
49
+ },
50
+ ensure_ascii=False,
51
+ )
52
+
53
+ return (
54
+ "Note collaboration guide:\n"
55
+ f"- No note has been created for this task yet, please first call: [TOOL_CALL:note:{create_payload}].\n"
56
+ "- After successful creation, record the returned note_id and reuse it in all subsequent updates.\n"
57
+ "- After syncing notes, then output the summary for the user.\n"
58
+ )
backend/src/services/planner.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Service responsible for converting the research topic into actionable tasks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import re
8
+ from typing import Any, List, Optional
9
+
10
+ from models import TodoItem
11
+ from config import Configuration
12
+ from prompts import get_current_date, todo_planner_instructions
13
+ from utils import strip_thinking_tokens
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ TOOL_CALL_PATTERN = re.compile(
18
+ r"\[TOOL_CALL:(?P<tool>[^:]+):(?P<body>[^\]]+)\]",
19
+ re.IGNORECASE,
20
+ )
21
+
22
+ class PlanningService:
23
+ """Wraps the planner agent to produce structured TODO items."""
24
+
25
+ def __init__(self, planner_agent: ToolAwareSimpleAgent, config: Configuration) -> None:
26
+ self._agent = planner_agent
27
+ self._config = config
28
+
29
+ def plan_todo_list(self, state: SummaryState) -> List[TodoItem]:
30
+ """Ask the planner agent to break the topic into actionable tasks."""
31
+
32
+ prompt = todo_planner_instructions.format(
33
+ current_date=get_current_date(),
34
+ research_topic=state.research_topic,
35
+ )
36
+
37
+ response = self._agent.run(prompt)
38
+ self._agent.clear_history()
39
+
40
+ logger.info("Planner raw output (truncated): %s", response[:500])
41
+
42
+ tasks_payload = self._extract_tasks(response)
43
+ todo_items: List[TodoItem] = []
44
+
45
+ for idx, item in enumerate(tasks_payload, start=1):
46
+ title = str(item.get("title") or f"Task{idx}").strip()
47
+ intent = str(item.get("intent") or "Focus on key issues of the topic").strip()
48
+ query = str(item.get("query") or state.research_topic).strip()
49
+
50
+ if not query:
51
+ query = state.research_topic
52
+
53
+ task = TodoItem(
54
+ id=idx,
55
+ title=title,
56
+ intent=intent,
57
+ query=query,
58
+ )
59
+ todo_items.append(task)
60
+
61
+ state.todo_items = todo_items
62
+
63
+ titles = [task.title for task in todo_items]
64
+ logger.info("Planner produced %d tasks: %s", len(todo_items), titles)
65
+ return todo_items
66
+
67
+ @staticmethod
68
+ def create_fallback_task(state: SummaryState) -> TodoItem:
69
+ """Create a minimal fallback task when planning failed."""
70
+
71
+ return TodoItem(
72
+ id=1,
73
+ title="Basic Background Overview",
74
+ intent="Collect core background and latest developments on the topic",
75
+ query=f"{state.research_topic} latest developments" if state.research_topic else "Basic background overview",
76
+ )
77
+
78
+ # ------------------------------------------------------------------
79
+ # Parsing helpers
80
+ # ------------------------------------------------------------------
81
+ def _extract_tasks(self, raw_response: str) -> List[dict[str, Any]]:
82
+ """Parse planner output into a list of task dictionaries."""
83
+
84
+ text = raw_response.strip()
85
+ if self._config.strip_thinking_tokens:
86
+ text = strip_thinking_tokens(text)
87
+
88
+ json_payload = self._extract_json_payload(text)
89
+ tasks: List[dict[str, Any]] = []
90
+
91
+ if isinstance(json_payload, dict):
92
+ candidate = json_payload.get("tasks")
93
+ if isinstance(candidate, list):
94
+ for item in candidate:
95
+ if isinstance(item, dict):
96
+ tasks.append(item)
97
+ elif isinstance(json_payload, list):
98
+ for item in json_payload:
99
+ if isinstance(item, dict):
100
+ tasks.append(item)
101
+
102
+ if not tasks:
103
+ tool_payload = self._extract_tool_payload(text)
104
+ if tool_payload and isinstance(tool_payload.get("tasks"), list):
105
+ for item in tool_payload["tasks"]:
106
+ if isinstance(item, dict):
107
+ tasks.append(item)
108
+
109
+ return tasks
110
+
111
+ def _extract_json_payload(self, text: str) -> Optional[dict[str, Any] | list]:
112
+ """Try to locate and parse a JSON object or array from the text."""
113
+
114
+ start = text.find("{")
115
+ end = text.rfind("}")
116
+ if start != -1 and end != -1 and end > start:
117
+ candidate = text[start : end + 1]
118
+ try:
119
+ return json.loads(candidate)
120
+ except json.JSONDecodeError:
121
+ pass
122
+
123
+ start = text.find("[")
124
+ end = text.rfind("]")
125
+ if start != -1 and end != -1 and end > start:
126
+ candidate = text[start : end + 1]
127
+ try:
128
+ return json.loads(candidate)
129
+ except json.JSONDecodeError:
130
+ return None
131
+
132
+ return None
133
+
134
+ def _extract_tool_payload(self, text: str) -> Optional[dict[str, Any]]:
135
+ """Parse the first TOOL_CALL expression in the output."""
136
+
137
+ match = TOOL_CALL_PATTERN.search(text)
138
+ if not match:
139
+ return None
140
+
141
+ body = match.group("body")
142
+
143
+ try:
144
+ payload = json.loads(body)
145
+ if isinstance(payload, dict):
146
+ return payload
147
+ except json.JSONDecodeError:
148
+ pass
149
+
150
+ parts = [segment.strip() for segment in body.split(",") if segment.strip()]
151
+ payload: dict[str, Any] = {}
152
+ for part in parts:
153
+ if "=" not in part:
154
+ continue
155
+ key, value = part.split("=", 1)
156
+ payload[key.strip()] = value.strip().strip('"').strip("'")
157
+
158
+ return payload or None
backend/src/services/reporter.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Service that consolidates task results into the final report."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from langchain_openai import ChatOpenAI
8
+ from langchain_core.messages import HumanMessage, SystemMessage
9
+
10
+ from models import SummaryState
11
+ from config import Configuration
12
+ from utils import strip_thinking_tokens
13
+ from services.text_processing import strip_tool_calls
14
+ from prompts import report_writer_instructions
15
+
16
+
17
+ def generate_report(
18
+ llm: ChatOpenAI,
19
+ state: SummaryState,
20
+ config: Configuration,
21
+ ) -> str:
22
+ """Generate a structured report based on completed tasks."""
23
+
24
+ tasks_block = []
25
+ for task in state.todo_items:
26
+ summary_block = task.summary or "No information available"
27
+ sources_block = task.sources_summary or "No sources available"
28
+ tasks_block.append(
29
+ f"### Task {task.id}: {task.title}\n"
30
+ f"- Objective: {task.intent}\n"
31
+ f"- Search query: {task.query}\n"
32
+ f"- Status: {task.status}\n"
33
+ f"- Summary:\n{summary_block}\n"
34
+ f"- Sources:\n{sources_block}\n"
35
+ )
36
+
37
+ note_references = []
38
+ for task in state.todo_items:
39
+ if task.note_id:
40
+ note_references.append(
41
+ f"- Task {task.id} '{task.title}': note_id={task.note_id}"
42
+ )
43
+
44
+ notes_section = "\n".join(note_references) if note_references else "- No task notes available"
45
+
46
+ read_template = json.dumps({"action": "read", "note_id": "<note_id>"}, ensure_ascii=False)
47
+ create_conclusion_template = json.dumps(
48
+ {
49
+ "action": "create",
50
+ "title": f"Research Report: {state.research_topic}",
51
+ "note_type": "conclusion",
52
+ "tags": ["deep_research", "report"],
53
+ "content": "Please summarize the final report highlights here",
54
+ },
55
+ ensure_ascii=False,
56
+ )
57
+
58
+ prompt = (
59
+ f"Research topic: {state.research_topic}\n"
60
+ f"Task overview:\n{''.join(tasks_block)}\n"
61
+ f"Available task notes:\n{notes_section}\n"
62
+ f"Please use the format [TOOL_CALL:note:{read_template}] to read each task note, then compile all information to write the report.\n"
63
+ f"If you need to output a summary conclusion, you can call [TOOL_CALL:note:{create_conclusion_template}] to save report highlights."
64
+ )
65
+
66
+ messages = [
67
+ SystemMessage(content=report_writer_instructions.strip()),
68
+ HumanMessage(content=prompt),
69
+ ]
70
+
71
+ response = llm.invoke(messages)
72
+ report_text = response.content.strip()
73
+
74
+ if config.strip_thinking_tokens:
75
+ report_text = strip_thinking_tokens(report_text)
76
+
77
+ report_text = strip_tool_calls(report_text).strip()
78
+
79
+ return report_text or "Report generation failed, please check input."
backend/src/services/search.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Search dispatch helpers using DuckDuckGo and Tavily."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, Optional, Tuple
7
+
8
+ from config import Configuration
9
+ from utils import (
10
+ deduplicate_and_format_sources,
11
+ format_sources,
12
+ get_config_value,
13
+ )
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ MAX_TOKENS_PER_SOURCE = 2000
18
+
19
+
20
+ def _search_duckduckgo(query: str, max_results: int = 5) -> dict[str, Any]:
21
+ """Execute search using DuckDuckGo."""
22
+ try:
23
+ from ddgs import DDGS
24
+
25
+ with DDGS() as ddgs:
26
+ results = list(ddgs.text(query, max_results=max_results))
27
+
28
+ formatted_results = []
29
+ for r in results:
30
+ formatted_results.append({
31
+ "title": r.get("title", ""),
32
+ "url": r.get("href", r.get("link", "")),
33
+ "content": r.get("body", r.get("snippet", "")),
34
+ })
35
+
36
+ return {
37
+ "results": formatted_results,
38
+ "backend": "duckduckgo",
39
+ "answer": None,
40
+ "notices": [],
41
+ }
42
+ except Exception as e:
43
+ logger.exception("DuckDuckGo search failed: %s", e)
44
+ return {
45
+ "results": [],
46
+ "backend": "duckduckgo",
47
+ "answer": None,
48
+ "notices": [f"Search failed: {str(e)}"],
49
+ }
50
+
51
+
52
+ def _search_tavily(query: str, max_results: int = 5) -> dict[str, Any]:
53
+ """Execute search using Tavily API."""
54
+ try:
55
+ import os
56
+ from tavily import TavilyClient
57
+
58
+ api_key = os.getenv("TAVILY_API_KEY")
59
+ if not api_key:
60
+ return {
61
+ "results": [],
62
+ "backend": "tavily",
63
+ "answer": None,
64
+ "notices": ["Missing TAVILY_API_KEY environment variable"],
65
+ }
66
+
67
+ client = TavilyClient(api_key=api_key)
68
+ response = client.search(query, max_results=max_results)
69
+
70
+ formatted_results = []
71
+ for r in response.get("results", []):
72
+ formatted_results.append({
73
+ "title": r.get("title", ""),
74
+ "url": r.get("url", ""),
75
+ "content": r.get("content", ""),
76
+ "raw_content": r.get("raw_content"),
77
+ })
78
+
79
+ return {
80
+ "results": formatted_results,
81
+ "backend": "tavily",
82
+ "answer": response.get("answer"),
83
+ "notices": [],
84
+ }
85
+ except Exception as e:
86
+ logger.exception("Tavily search failed: %s", e)
87
+ return {
88
+ "results": [],
89
+ "backend": "tavily",
90
+ "answer": None,
91
+ "notices": [f"Search failed: {str(e)}"],
92
+ }
93
+
94
+
95
+ def _search_perplexity(query: str, max_results: int = 5) -> dict[str, Any]:
96
+ """Execute search using Perplexity API."""
97
+ try:
98
+ import os
99
+ from openai import OpenAI
100
+
101
+ api_key = os.getenv("PERPLEXITY_API_KEY")
102
+ if not api_key:
103
+ return {
104
+ "results": [],
105
+ "backend": "perplexity",
106
+ "answer": None,
107
+ "notices": ["Missing PERPLEXITY_API_KEY environment variable"],
108
+ }
109
+
110
+ client = OpenAI(api_key=api_key, base_url="https://api.perplexity.ai")
111
+
112
+ response = client.chat.completions.create(
113
+ model="llama-3.1-sonar-small-128k-online",
114
+ messages=[{"role": "user", "content": query}],
115
+ )
116
+
117
+ answer = response.choices[0].message.content if response.choices else None
118
+
119
+ # Perplexity returns answer text, not structured results
120
+ return {
121
+ "results": [{
122
+ "title": "Perplexity Answer",
123
+ "url": "",
124
+ "content": answer or "",
125
+ }] if answer else [],
126
+ "backend": "perplexity",
127
+ "answer": answer,
128
+ "notices": [],
129
+ }
130
+ except Exception as e:
131
+ logger.exception("Perplexity search failed: %s", e)
132
+ return {
133
+ "results": [],
134
+ "backend": "perplexity",
135
+ "answer": None,
136
+ "notices": [f"Search failed: {str(e)}"],
137
+ }
138
+
139
+
140
+ def _search_searxng(query: str, max_results: int = 5, base_url: str = "http://localhost:8888") -> dict[str, Any]:
141
+ """Execute search using SearXNG instance."""
142
+ try:
143
+ import requests
144
+
145
+ params = {
146
+ "q": query,
147
+ "format": "json",
148
+ "engines": "google,bing,duckduckgo",
149
+ }
150
+
151
+ response = requests.get(f"{base_url}/search", params=params, timeout=30)
152
+ response.raise_for_status()
153
+ data = response.json()
154
+
155
+ formatted_results = []
156
+ for r in data.get("results", [])[:max_results]:
157
+ formatted_results.append({
158
+ "title": r.get("title", ""),
159
+ "url": r.get("url", ""),
160
+ "content": r.get("content", ""),
161
+ })
162
+
163
+ return {
164
+ "results": formatted_results,
165
+ "backend": "searxng",
166
+ "answer": None,
167
+ "notices": [],
168
+ }
169
+ except Exception as e:
170
+ logger.exception("SearXNG search failed: %s", e)
171
+ return {
172
+ "results": [],
173
+ "backend": "searxng",
174
+ "answer": None,
175
+ "notices": [f"Search failed: {str(e)}"],
176
+ }
177
+
178
+
179
+ def dispatch_search(
180
+ query: str,
181
+ config: Configuration,
182
+ loop_count: int,
183
+ ) -> Tuple[dict[str, Any] | None, list[str], Optional[str], str]:
184
+ """Execute configured search backend and normalize response payload."""
185
+
186
+ search_api = get_config_value(config.search_api)
187
+ max_results = 5
188
+
189
+ try:
190
+ if search_api == "tavily":
191
+ payload = _search_tavily(query, max_results)
192
+ elif search_api == "perplexity":
193
+ payload = _search_perplexity(query, max_results)
194
+ elif search_api == "searxng":
195
+ payload = _search_searxng(query, max_results)
196
+ elif search_api == "advanced":
197
+ # Try Tavily first, fall back to DuckDuckGo
198
+ payload = _search_tavily(query, max_results)
199
+ if not payload.get("results"):
200
+ payload = _search_duckduckgo(query, max_results)
201
+ else:
202
+ # Default to DuckDuckGo
203
+ payload = _search_duckduckgo(query, max_results)
204
+ except Exception as exc:
205
+ logger.exception("Search backend %s failed: %s", search_api, exc)
206
+ raise
207
+
208
+ notices = list(payload.get("notices") or [])
209
+ backend_label = str(payload.get("backend") or search_api)
210
+ answer_text = payload.get("answer")
211
+ results = payload.get("results", [])
212
+
213
+ if notices:
214
+ for notice in notices:
215
+ logger.info("Search notice (%s): %s", backend_label, notice)
216
+
217
+ logger.info(
218
+ "Search backend=%s resolved_backend=%s answer=%s results=%s",
219
+ search_api,
220
+ backend_label,
221
+ bool(answer_text),
222
+ len(results),
223
+ )
224
+
225
+ return payload, notices, answer_text, backend_label
226
+
227
+
228
+ def prepare_research_context(
229
+ search_result: dict[str, Any] | None,
230
+ answer_text: Optional[str],
231
+ config: Configuration,
232
+ ) -> tuple[str, str]:
233
+ """Build structured context and source summary for downstream agents."""
234
+
235
+ sources_summary = format_sources(search_result)
236
+ context = deduplicate_and_format_sources(
237
+ search_result or {"results": []},
238
+ max_tokens_per_source=MAX_TOKENS_PER_SOURCE,
239
+ fetch_full_page=config.fetch_full_page,
240
+ )
241
+
242
+ if answer_text:
243
+ context = f"AI Direct Answer:\n{answer_text}\n\n{context}"
244
+
245
+ return sources_summary, context
backend/src/services/summarizer.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task summarization utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import Tuple, Callable
7
+
8
+ from langchain_openai import ChatOpenAI
9
+ from langchain_core.messages import HumanMessage, SystemMessage
10
+
11
+ from models import SummaryState, TodoItem
12
+ from config import Configuration
13
+ from utils import strip_thinking_tokens
14
+ from services.notes import build_note_guidance
15
+ from services.text_processing import strip_tool_calls
16
+ from prompts import task_summarizer_instructions
17
+
18
+
19
+ def summarize_task(
20
+ llm: ChatOpenAI,
21
+ state: SummaryState,
22
+ task: TodoItem,
23
+ context: str,
24
+ config: Configuration,
25
+ ) -> str:
26
+ """Generate a task-specific summary using the LLM."""
27
+
28
+ prompt = _build_prompt(state, task, context)
29
+
30
+ messages = [
31
+ SystemMessage(content=task_summarizer_instructions.strip()),
32
+ HumanMessage(content=prompt),
33
+ ]
34
+
35
+ response = llm.invoke(messages)
36
+ summary_text = response.content.strip()
37
+
38
+ if config.strip_thinking_tokens:
39
+ summary_text = strip_thinking_tokens(summary_text)
40
+
41
+ summary_text = strip_tool_calls(summary_text).strip()
42
+
43
+ return summary_text or "No information available"
44
+
45
+
46
+ def stream_task_summary(
47
+ llm: ChatOpenAI,
48
+ state: SummaryState,
49
+ task: TodoItem,
50
+ context: str,
51
+ config: Configuration,
52
+ ) -> Tuple[Iterator[str], Callable[[], str]]:
53
+ """Stream the summary text for a task while collecting full output."""
54
+
55
+ prompt = _build_prompt(state, task, context)
56
+ remove_thinking = config.strip_thinking_tokens
57
+ raw_buffer = ""
58
+ visible_output = ""
59
+ emit_index = 0
60
+
61
+ messages = [
62
+ SystemMessage(content=task_summarizer_instructions.strip()),
63
+ HumanMessage(content=prompt),
64
+ ]
65
+
66
+ def flush_visible() -> Iterator[str]:
67
+ nonlocal emit_index, raw_buffer
68
+ while True:
69
+ start = raw_buffer.find("<think>", emit_index)
70
+ if start == -1:
71
+ if emit_index < len(raw_buffer):
72
+ segment = raw_buffer[emit_index:]
73
+ emit_index = len(raw_buffer)
74
+ if segment:
75
+ yield segment
76
+ break
77
+
78
+ if start > emit_index:
79
+ segment = raw_buffer[emit_index:start]
80
+ emit_index = start
81
+ if segment:
82
+ yield segment
83
+
84
+ end = raw_buffer.find("</think>", start)
85
+ if end == -1:
86
+ break
87
+ emit_index = end + len("</think>")
88
+
89
+ def generator() -> Iterator[str]:
90
+ nonlocal raw_buffer, visible_output, emit_index
91
+ try:
92
+ for chunk in llm.stream(messages):
93
+ chunk_text = chunk.content
94
+ if not chunk_text:
95
+ continue
96
+ raw_buffer += chunk_text
97
+ if remove_thinking:
98
+ for segment in flush_visible():
99
+ visible_output += segment
100
+ if segment:
101
+ yield segment
102
+ else:
103
+ visible_output += chunk_text
104
+ if chunk_text:
105
+ yield chunk_text
106
+ finally:
107
+ if remove_thinking:
108
+ for segment in flush_visible():
109
+ visible_output += segment
110
+ if segment:
111
+ yield segment
112
+
113
+ def get_summary() -> str:
114
+ if remove_thinking:
115
+ cleaned = strip_thinking_tokens(visible_output)
116
+ else:
117
+ cleaned = visible_output
118
+
119
+ return strip_tool_calls(cleaned).strip()
120
+
121
+ return generator(), get_summary
122
+
123
+
124
+ def _build_prompt(state: SummaryState, task: TodoItem, context: str) -> str:
125
+ """Construct the summarization prompt shared by both modes."""
126
+
127
+ return (
128
+ f"Task topic: {state.research_topic}\n"
129
+ f"Task name: {task.title}\n"
130
+ f"Task objective: {task.intent}\n"
131
+ f"Search query: {task.query}\n"
132
+ f"Task context:\n{context}\n"
133
+ f"{build_note_guidance(task)}\n"
134
+ "Please follow the above collaboration requirements to sync notes first, then return a user-facing Markdown summary (still following the task summary template)."
135
+ )
backend/src/services/text_processing.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utility helpers for normalizing agent generated text."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+
8
+ def strip_tool_calls(text: str) -> str:
9
+ """Remove tool call markers from text."""
10
+
11
+ if not text:
12
+ return text
13
+
14
+ pattern = re.compile(r"\[TOOL_CALL:[^\]]+\]")
15
+ return pattern.sub("", text)
16
+
backend/src/services/tool_events.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utility for collecting and exposing tool call events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from threading import Lock
10
+ from typing import Any, Callable, Optional
11
+
12
+ from models import SummaryState, TodoItem
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class ToolCallEvent:
19
+ """Internal representation of a tool call event."""
20
+
21
+ id: int
22
+ agent: str
23
+ tool: str
24
+ raw_parameters: str
25
+ parsed_parameters: dict[str, Any]
26
+ result: str
27
+ task_id: Optional[int]
28
+ note_id: Optional[str]
29
+
30
+
31
+ class ToolCallTracker:
32
+ """Collects tool call events and converts them to SSE payloads."""
33
+
34
+ def __init__(self, notes_workspace: Optional[str]) -> None:
35
+ self._notes_workspace = notes_workspace
36
+ self._events: list[ToolCallEvent] = []
37
+ self._cursor = 0
38
+ self._lock = Lock()
39
+ self._event_sink: Optional[Callable[[dict[str, Any]], None]] = None
40
+
41
+ def record(self, payload: dict[str, Any]) -> None:
42
+ """Record a tool call event for logging and frontend display."""
43
+
44
+ agent_name = str(payload.get("agent_name") or payload.get("agent") or "unknown")
45
+ tool_name = str(payload.get("tool_name") or payload.get("tool") or "unknown")
46
+ raw_parameters = str(payload.get("raw_parameters") or "")
47
+ parsed_parameters = payload.get("parsed_parameters") or payload.get("parameters") or {}
48
+ result_text = str(payload.get("result") or "")
49
+
50
+ if not isinstance(parsed_parameters, dict):
51
+ parsed_parameters = {}
52
+
53
+ task_id = self._infer_task_id(parsed_parameters)
54
+ note_id: Optional[str] = payload.get("note_id")
55
+
56
+ if tool_name == "note" and note_id is None:
57
+ note_id = parsed_parameters.get("note_id")
58
+ if note_id is None:
59
+ note_id = self._extract_note_id(result_text)
60
+
61
+ event = ToolCallEvent(
62
+ id=len(self._events) + 1,
63
+ agent=agent_name,
64
+ tool=tool_name,
65
+ raw_parameters=raw_parameters,
66
+ parsed_parameters=parsed_parameters,
67
+ result=result_text,
68
+ task_id=task_id,
69
+ note_id=note_id,
70
+ )
71
+
72
+ with self._lock:
73
+ self._events.append(event)
74
+
75
+ logger.info(
76
+ "Tool call recorded: agent=%s tool=%s task_id=%s note_id=%s parsed_parameters=%s",
77
+ agent_name,
78
+ tool_name,
79
+ task_id,
80
+ note_id,
81
+ parsed_parameters,
82
+ )
83
+
84
+ sink = self._event_sink
85
+ if sink:
86
+ sink(self._build_payload(event, step=None))
87
+
88
+ # ------------------------------------------------------------------
89
+ # Draining helpers
90
+ # ------------------------------------------------------------------
91
+ def drain(self, state: Optional[SummaryState] = None, *, step: Optional[int] = None) -> list[dict[str, Any]]:
92
+ """Extract unconsumed tool call events and sync task note_ids."""
93
+
94
+ with self._lock:
95
+ if self._cursor >= len(self._events):
96
+ return []
97
+ new_events = self._events[self._cursor :]
98
+ self._cursor = len(self._events)
99
+
100
+ if state and state.todo_items:
101
+ for event in new_events:
102
+ task_id = event.task_id
103
+ note_id = event.note_id
104
+ if task_id is None or not note_id:
105
+ continue
106
+ self._attach_note_to_task(state.todo_items, task_id, note_id)
107
+
108
+ payloads: list[dict[str, Any]] = []
109
+ for event in new_events:
110
+ payload = self._build_payload(event, step=step)
111
+ payloads.append(payload)
112
+
113
+ return payloads
114
+
115
+ def reset(self) -> None:
116
+ """Clear recorded events."""
117
+
118
+ with self._lock:
119
+ self._events.clear()
120
+ self._cursor = 0
121
+
122
+ def as_dicts(self) -> list[dict[str, Any]]:
123
+ """Expose a snapshot of raw events for backwards compatibility."""
124
+
125
+ with self._lock:
126
+ return [
127
+ {
128
+ "id": event.id,
129
+ "agent": event.agent,
130
+ "tool": event.tool,
131
+ "raw_parameters": event.raw_parameters,
132
+ "parsed_parameters": event.parsed_parameters,
133
+ "result": event.result,
134
+ "task_id": event.task_id,
135
+ "note_id": event.note_id,
136
+ }
137
+ for event in self._events
138
+ ]
139
+
140
+ def set_event_sink(self, sink: Optional[Callable[[dict[str, Any]], None]]) -> None:
141
+ """Register a callback for immediate tool event notifications."""
142
+
143
+ self._event_sink = sink
144
+
145
+ def _build_payload(self, event: ToolCallEvent, step: Optional[int]) -> dict[str, Any]:
146
+ payload = {
147
+ "type": "tool_call",
148
+ "event_id": event.id,
149
+ "agent": event.agent,
150
+ "tool": event.tool,
151
+ "parameters": event.parsed_parameters,
152
+ "result": event.result,
153
+ "task_id": event.task_id,
154
+ "note_id": event.note_id,
155
+ }
156
+ if event.note_id and self._notes_workspace:
157
+ note_path = Path(self._notes_workspace) / f"{event.note_id}.md"
158
+ payload["note_path"] = str(note_path)
159
+ if step is not None:
160
+ payload["step"] = step
161
+ return payload
162
+
163
+ # ------------------------------------------------------------------
164
+ # Internal helpers
165
+ # ------------------------------------------------------------------
166
+ def _attach_note_to_task(self, tasks: list[TodoItem], task_id: int, note_id: str) -> None:
167
+ """Update matching TODO item with note metadata."""
168
+
169
+ for task in tasks:
170
+ if task.id != task_id:
171
+ continue
172
+
173
+ if task.note_id != note_id:
174
+ task.note_id = note_id
175
+ if self._notes_workspace:
176
+ task.note_path = str(Path(self._notes_workspace) / f"{note_id}.md")
177
+ elif task.note_path is None and self._notes_workspace:
178
+ task.note_path = str(Path(self._notes_workspace) / f"{note_id}.md")
179
+ break
180
+
181
+ def _infer_task_id(self, parameters: dict[str, Any]) -> Optional[int]:
182
+ """Attempt to infer task_id from tool parameters."""
183
+
184
+ if not parameters:
185
+ return None
186
+
187
+ if "task_id" in parameters:
188
+ try:
189
+ return int(parameters["task_id"])
190
+ except (TypeError, ValueError):
191
+ pass
192
+
193
+ tags = parameters.get("tags")
194
+ if isinstance(tags, list):
195
+ for tag in tags:
196
+ match = re.search(r"task_(\d+)", str(tag))
197
+ if match:
198
+ return int(match.group(1))
199
+
200
+ title = parameters.get("title")
201
+ if isinstance(title, str):
202
+ match = re.search(r"Task\s*(\d+)", title)
203
+ if match:
204
+ return int(match.group(1))
205
+
206
+ return None
207
+
208
+ def _extract_note_id(self, response: str) -> Optional[str]:
209
+ if not response:
210
+ return None
211
+
212
+ match = re.search(r"ID:\s*([^\n]+)", response)
213
+ if match:
214
+ return match.group(1).strip()
215
+ return None
backend/src/utils.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utility helpers shared across deep researcher services."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, Dict, List, Union
7
+
8
+ CHARS_PER_TOKEN = 4
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def get_config_value(value: Any) -> str:
14
+ """Return configuration value as plain string."""
15
+
16
+ return value if isinstance(value, str) else value.value
17
+
18
+
19
+ def strip_thinking_tokens(text: str) -> str:
20
+ """Remove ``<think>`` sections from model responses."""
21
+
22
+ while "<think>" in text and "</think>" in text:
23
+ start = text.find("<think>")
24
+ end = text.find("</think>") + len("</think>")
25
+ text = text[:start] + text[end:]
26
+ return text
27
+
28
+
29
+ def deduplicate_and_format_sources(
30
+ search_response: Dict[str, Any] | List[Dict[str, Any]],
31
+ max_tokens_per_source: int,
32
+ *,
33
+ fetch_full_page: bool = False,
34
+ ) -> str:
35
+ """Format and deduplicate search results for downstream prompting."""
36
+
37
+ if isinstance(search_response, dict):
38
+ sources_list = search_response.get("results", [])
39
+ else:
40
+ sources_list = search_response
41
+
42
+ unique_sources: dict[str, Dict[str, Any]] = {}
43
+ for source in sources_list:
44
+ url = source.get("url")
45
+ if not url:
46
+ continue
47
+ if url not in unique_sources:
48
+ unique_sources[url] = source
49
+
50
+ formatted_parts: List[str] = []
51
+ for source in unique_sources.values():
52
+ title = source.get("title") or source.get("url", "")
53
+ content = source.get("content", "")
54
+ formatted_parts.append(f"Source: {title}\n\n")
55
+ formatted_parts.append(f"URL: {source.get('url', '')}\n\n")
56
+ formatted_parts.append(f"Content: {content}\n\n")
57
+
58
+ if fetch_full_page:
59
+ raw_content = source.get("raw_content")
60
+ if raw_content is None:
61
+ logger.debug("raw_content missing for %s", source.get("url", ""))
62
+ raw_content = ""
63
+ char_limit = max_tokens_per_source * CHARS_PER_TOKEN
64
+ if len(raw_content) > char_limit:
65
+ raw_content = f"{raw_content[:char_limit]}... [truncated]"
66
+ formatted_parts.append(
67
+ f"Full content limited to {max_tokens_per_source} tokens: {raw_content}\n\n"
68
+ )
69
+
70
+ return "".join(formatted_parts).strip()
71
+
72
+
73
+ def format_sources(search_results: Dict[str, Any] | None) -> str:
74
+ """Return bullet list summarising search sources."""
75
+
76
+ if not search_results:
77
+ return ""
78
+
79
+ results = search_results.get("results", [])
80
+ return "\n".join(
81
+ f"* {item.get('title', item.get('url', ''))} : {item.get('url', '')}"
82
+ for item in results
83
+ if item.get("url")
84
+ )
frontend/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .DS_Store
frontend/index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>HelloAgents Deep Research Assistant</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
frontend/package-lock.json ADDED
@@ -0,0 +1,1758 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "helloagents-deepresearch-frontend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "helloagents-deepresearch-frontend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "axios": "^1.7.9",
12
+ "vue": "^3.5.13"
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^22.10.5",
16
+ "@vitejs/plugin-vue": "^5.2.1",
17
+ "typescript": "^5.7.3",
18
+ "vite": "^6.0.7",
19
+ "vue-tsc": "^2.2.0"
20
+ }
21
+ },
22
+ "node_modules/@babel/helper-string-parser": {
23
+ "version": "7.27.1",
24
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
25
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=6.9.0"
29
+ }
30
+ },
31
+ "node_modules/@babel/helper-validator-identifier": {
32
+ "version": "7.27.1",
33
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
34
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
35
+ "license": "MIT",
36
+ "engines": {
37
+ "node": ">=6.9.0"
38
+ }
39
+ },
40
+ "node_modules/@babel/parser": {
41
+ "version": "7.28.4",
42
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
43
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
44
+ "license": "MIT",
45
+ "dependencies": {
46
+ "@babel/types": "^7.28.4"
47
+ },
48
+ "bin": {
49
+ "parser": "bin/babel-parser.js"
50
+ },
51
+ "engines": {
52
+ "node": ">=6.0.0"
53
+ }
54
+ },
55
+ "node_modules/@babel/types": {
56
+ "version": "7.28.4",
57
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
58
+ "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
59
+ "license": "MIT",
60
+ "dependencies": {
61
+ "@babel/helper-string-parser": "^7.27.1",
62
+ "@babel/helper-validator-identifier": "^7.27.1"
63
+ },
64
+ "engines": {
65
+ "node": ">=6.9.0"
66
+ }
67
+ },
68
+ "node_modules/@esbuild/aix-ppc64": {
69
+ "version": "0.25.11",
70
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
71
+ "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==",
72
+ "cpu": [
73
+ "ppc64"
74
+ ],
75
+ "dev": true,
76
+ "license": "MIT",
77
+ "optional": true,
78
+ "os": [
79
+ "aix"
80
+ ],
81
+ "engines": {
82
+ "node": ">=18"
83
+ }
84
+ },
85
+ "node_modules/@esbuild/android-arm": {
86
+ "version": "0.25.11",
87
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz",
88
+ "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==",
89
+ "cpu": [
90
+ "arm"
91
+ ],
92
+ "dev": true,
93
+ "license": "MIT",
94
+ "optional": true,
95
+ "os": [
96
+ "android"
97
+ ],
98
+ "engines": {
99
+ "node": ">=18"
100
+ }
101
+ },
102
+ "node_modules/@esbuild/android-arm64": {
103
+ "version": "0.25.11",
104
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz",
105
+ "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==",
106
+ "cpu": [
107
+ "arm64"
108
+ ],
109
+ "dev": true,
110
+ "license": "MIT",
111
+ "optional": true,
112
+ "os": [
113
+ "android"
114
+ ],
115
+ "engines": {
116
+ "node": ">=18"
117
+ }
118
+ },
119
+ "node_modules/@esbuild/android-x64": {
120
+ "version": "0.25.11",
121
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz",
122
+ "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==",
123
+ "cpu": [
124
+ "x64"
125
+ ],
126
+ "dev": true,
127
+ "license": "MIT",
128
+ "optional": true,
129
+ "os": [
130
+ "android"
131
+ ],
132
+ "engines": {
133
+ "node": ">=18"
134
+ }
135
+ },
136
+ "node_modules/@esbuild/darwin-arm64": {
137
+ "version": "0.25.11",
138
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz",
139
+ "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==",
140
+ "cpu": [
141
+ "arm64"
142
+ ],
143
+ "dev": true,
144
+ "license": "MIT",
145
+ "optional": true,
146
+ "os": [
147
+ "darwin"
148
+ ],
149
+ "engines": {
150
+ "node": ">=18"
151
+ }
152
+ },
153
+ "node_modules/@esbuild/darwin-x64": {
154
+ "version": "0.25.11",
155
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz",
156
+ "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==",
157
+ "cpu": [
158
+ "x64"
159
+ ],
160
+ "dev": true,
161
+ "license": "MIT",
162
+ "optional": true,
163
+ "os": [
164
+ "darwin"
165
+ ],
166
+ "engines": {
167
+ "node": ">=18"
168
+ }
169
+ },
170
+ "node_modules/@esbuild/freebsd-arm64": {
171
+ "version": "0.25.11",
172
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz",
173
+ "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==",
174
+ "cpu": [
175
+ "arm64"
176
+ ],
177
+ "dev": true,
178
+ "license": "MIT",
179
+ "optional": true,
180
+ "os": [
181
+ "freebsd"
182
+ ],
183
+ "engines": {
184
+ "node": ">=18"
185
+ }
186
+ },
187
+ "node_modules/@esbuild/freebsd-x64": {
188
+ "version": "0.25.11",
189
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz",
190
+ "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==",
191
+ "cpu": [
192
+ "x64"
193
+ ],
194
+ "dev": true,
195
+ "license": "MIT",
196
+ "optional": true,
197
+ "os": [
198
+ "freebsd"
199
+ ],
200
+ "engines": {
201
+ "node": ">=18"
202
+ }
203
+ },
204
+ "node_modules/@esbuild/linux-arm": {
205
+ "version": "0.25.11",
206
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz",
207
+ "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==",
208
+ "cpu": [
209
+ "arm"
210
+ ],
211
+ "dev": true,
212
+ "license": "MIT",
213
+ "optional": true,
214
+ "os": [
215
+ "linux"
216
+ ],
217
+ "engines": {
218
+ "node": ">=18"
219
+ }
220
+ },
221
+ "node_modules/@esbuild/linux-arm64": {
222
+ "version": "0.25.11",
223
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz",
224
+ "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==",
225
+ "cpu": [
226
+ "arm64"
227
+ ],
228
+ "dev": true,
229
+ "license": "MIT",
230
+ "optional": true,
231
+ "os": [
232
+ "linux"
233
+ ],
234
+ "engines": {
235
+ "node": ">=18"
236
+ }
237
+ },
238
+ "node_modules/@esbuild/linux-ia32": {
239
+ "version": "0.25.11",
240
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz",
241
+ "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==",
242
+ "cpu": [
243
+ "ia32"
244
+ ],
245
+ "dev": true,
246
+ "license": "MIT",
247
+ "optional": true,
248
+ "os": [
249
+ "linux"
250
+ ],
251
+ "engines": {
252
+ "node": ">=18"
253
+ }
254
+ },
255
+ "node_modules/@esbuild/linux-loong64": {
256
+ "version": "0.25.11",
257
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz",
258
+ "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==",
259
+ "cpu": [
260
+ "loong64"
261
+ ],
262
+ "dev": true,
263
+ "license": "MIT",
264
+ "optional": true,
265
+ "os": [
266
+ "linux"
267
+ ],
268
+ "engines": {
269
+ "node": ">=18"
270
+ }
271
+ },
272
+ "node_modules/@esbuild/linux-mips64el": {
273
+ "version": "0.25.11",
274
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz",
275
+ "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==",
276
+ "cpu": [
277
+ "mips64el"
278
+ ],
279
+ "dev": true,
280
+ "license": "MIT",
281
+ "optional": true,
282
+ "os": [
283
+ "linux"
284
+ ],
285
+ "engines": {
286
+ "node": ">=18"
287
+ }
288
+ },
289
+ "node_modules/@esbuild/linux-ppc64": {
290
+ "version": "0.25.11",
291
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz",
292
+ "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==",
293
+ "cpu": [
294
+ "ppc64"
295
+ ],
296
+ "dev": true,
297
+ "license": "MIT",
298
+ "optional": true,
299
+ "os": [
300
+ "linux"
301
+ ],
302
+ "engines": {
303
+ "node": ">=18"
304
+ }
305
+ },
306
+ "node_modules/@esbuild/linux-riscv64": {
307
+ "version": "0.25.11",
308
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz",
309
+ "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==",
310
+ "cpu": [
311
+ "riscv64"
312
+ ],
313
+ "dev": true,
314
+ "license": "MIT",
315
+ "optional": true,
316
+ "os": [
317
+ "linux"
318
+ ],
319
+ "engines": {
320
+ "node": ">=18"
321
+ }
322
+ },
323
+ "node_modules/@esbuild/linux-s390x": {
324
+ "version": "0.25.11",
325
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz",
326
+ "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==",
327
+ "cpu": [
328
+ "s390x"
329
+ ],
330
+ "dev": true,
331
+ "license": "MIT",
332
+ "optional": true,
333
+ "os": [
334
+ "linux"
335
+ ],
336
+ "engines": {
337
+ "node": ">=18"
338
+ }
339
+ },
340
+ "node_modules/@esbuild/linux-x64": {
341
+ "version": "0.25.11",
342
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz",
343
+ "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==",
344
+ "cpu": [
345
+ "x64"
346
+ ],
347
+ "dev": true,
348
+ "license": "MIT",
349
+ "optional": true,
350
+ "os": [
351
+ "linux"
352
+ ],
353
+ "engines": {
354
+ "node": ">=18"
355
+ }
356
+ },
357
+ "node_modules/@esbuild/netbsd-arm64": {
358
+ "version": "0.25.11",
359
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz",
360
+ "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==",
361
+ "cpu": [
362
+ "arm64"
363
+ ],
364
+ "dev": true,
365
+ "license": "MIT",
366
+ "optional": true,
367
+ "os": [
368
+ "netbsd"
369
+ ],
370
+ "engines": {
371
+ "node": ">=18"
372
+ }
373
+ },
374
+ "node_modules/@esbuild/netbsd-x64": {
375
+ "version": "0.25.11",
376
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz",
377
+ "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==",
378
+ "cpu": [
379
+ "x64"
380
+ ],
381
+ "dev": true,
382
+ "license": "MIT",
383
+ "optional": true,
384
+ "os": [
385
+ "netbsd"
386
+ ],
387
+ "engines": {
388
+ "node": ">=18"
389
+ }
390
+ },
391
+ "node_modules/@esbuild/openbsd-arm64": {
392
+ "version": "0.25.11",
393
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz",
394
+ "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==",
395
+ "cpu": [
396
+ "arm64"
397
+ ],
398
+ "dev": true,
399
+ "license": "MIT",
400
+ "optional": true,
401
+ "os": [
402
+ "openbsd"
403
+ ],
404
+ "engines": {
405
+ "node": ">=18"
406
+ }
407
+ },
408
+ "node_modules/@esbuild/openbsd-x64": {
409
+ "version": "0.25.11",
410
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz",
411
+ "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==",
412
+ "cpu": [
413
+ "x64"
414
+ ],
415
+ "dev": true,
416
+ "license": "MIT",
417
+ "optional": true,
418
+ "os": [
419
+ "openbsd"
420
+ ],
421
+ "engines": {
422
+ "node": ">=18"
423
+ }
424
+ },
425
+ "node_modules/@esbuild/openharmony-arm64": {
426
+ "version": "0.25.11",
427
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz",
428
+ "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==",
429
+ "cpu": [
430
+ "arm64"
431
+ ],
432
+ "dev": true,
433
+ "license": "MIT",
434
+ "optional": true,
435
+ "os": [
436
+ "openharmony"
437
+ ],
438
+ "engines": {
439
+ "node": ">=18"
440
+ }
441
+ },
442
+ "node_modules/@esbuild/sunos-x64": {
443
+ "version": "0.25.11",
444
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz",
445
+ "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==",
446
+ "cpu": [
447
+ "x64"
448
+ ],
449
+ "dev": true,
450
+ "license": "MIT",
451
+ "optional": true,
452
+ "os": [
453
+ "sunos"
454
+ ],
455
+ "engines": {
456
+ "node": ">=18"
457
+ }
458
+ },
459
+ "node_modules/@esbuild/win32-arm64": {
460
+ "version": "0.25.11",
461
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz",
462
+ "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==",
463
+ "cpu": [
464
+ "arm64"
465
+ ],
466
+ "dev": true,
467
+ "license": "MIT",
468
+ "optional": true,
469
+ "os": [
470
+ "win32"
471
+ ],
472
+ "engines": {
473
+ "node": ">=18"
474
+ }
475
+ },
476
+ "node_modules/@esbuild/win32-ia32": {
477
+ "version": "0.25.11",
478
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz",
479
+ "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==",
480
+ "cpu": [
481
+ "ia32"
482
+ ],
483
+ "dev": true,
484
+ "license": "MIT",
485
+ "optional": true,
486
+ "os": [
487
+ "win32"
488
+ ],
489
+ "engines": {
490
+ "node": ">=18"
491
+ }
492
+ },
493
+ "node_modules/@esbuild/win32-x64": {
494
+ "version": "0.25.11",
495
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz",
496
+ "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==",
497
+ "cpu": [
498
+ "x64"
499
+ ],
500
+ "dev": true,
501
+ "license": "MIT",
502
+ "optional": true,
503
+ "os": [
504
+ "win32"
505
+ ],
506
+ "engines": {
507
+ "node": ">=18"
508
+ }
509
+ },
510
+ "node_modules/@jridgewell/sourcemap-codec": {
511
+ "version": "1.5.5",
512
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
513
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
514
+ "license": "MIT"
515
+ },
516
+ "node_modules/@rollup/rollup-android-arm-eabi": {
517
+ "version": "4.52.5",
518
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
519
+ "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
520
+ "cpu": [
521
+ "arm"
522
+ ],
523
+ "dev": true,
524
+ "license": "MIT",
525
+ "optional": true,
526
+ "os": [
527
+ "android"
528
+ ]
529
+ },
530
+ "node_modules/@rollup/rollup-android-arm64": {
531
+ "version": "4.52.5",
532
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
533
+ "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
534
+ "cpu": [
535
+ "arm64"
536
+ ],
537
+ "dev": true,
538
+ "license": "MIT",
539
+ "optional": true,
540
+ "os": [
541
+ "android"
542
+ ]
543
+ },
544
+ "node_modules/@rollup/rollup-darwin-arm64": {
545
+ "version": "4.52.5",
546
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
547
+ "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
548
+ "cpu": [
549
+ "arm64"
550
+ ],
551
+ "dev": true,
552
+ "license": "MIT",
553
+ "optional": true,
554
+ "os": [
555
+ "darwin"
556
+ ]
557
+ },
558
+ "node_modules/@rollup/rollup-darwin-x64": {
559
+ "version": "4.52.5",
560
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
561
+ "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
562
+ "cpu": [
563
+ "x64"
564
+ ],
565
+ "dev": true,
566
+ "license": "MIT",
567
+ "optional": true,
568
+ "os": [
569
+ "darwin"
570
+ ]
571
+ },
572
+ "node_modules/@rollup/rollup-freebsd-arm64": {
573
+ "version": "4.52.5",
574
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
575
+ "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
576
+ "cpu": [
577
+ "arm64"
578
+ ],
579
+ "dev": true,
580
+ "license": "MIT",
581
+ "optional": true,
582
+ "os": [
583
+ "freebsd"
584
+ ]
585
+ },
586
+ "node_modules/@rollup/rollup-freebsd-x64": {
587
+ "version": "4.52.5",
588
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
589
+ "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
590
+ "cpu": [
591
+ "x64"
592
+ ],
593
+ "dev": true,
594
+ "license": "MIT",
595
+ "optional": true,
596
+ "os": [
597
+ "freebsd"
598
+ ]
599
+ },
600
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
601
+ "version": "4.52.5",
602
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
603
+ "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
604
+ "cpu": [
605
+ "arm"
606
+ ],
607
+ "dev": true,
608
+ "license": "MIT",
609
+ "optional": true,
610
+ "os": [
611
+ "linux"
612
+ ]
613
+ },
614
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
615
+ "version": "4.52.5",
616
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
617
+ "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
618
+ "cpu": [
619
+ "arm"
620
+ ],
621
+ "dev": true,
622
+ "license": "MIT",
623
+ "optional": true,
624
+ "os": [
625
+ "linux"
626
+ ]
627
+ },
628
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
629
+ "version": "4.52.5",
630
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
631
+ "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
632
+ "cpu": [
633
+ "arm64"
634
+ ],
635
+ "dev": true,
636
+ "license": "MIT",
637
+ "optional": true,
638
+ "os": [
639
+ "linux"
640
+ ]
641
+ },
642
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
643
+ "version": "4.52.5",
644
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
645
+ "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
646
+ "cpu": [
647
+ "arm64"
648
+ ],
649
+ "dev": true,
650
+ "license": "MIT",
651
+ "optional": true,
652
+ "os": [
653
+ "linux"
654
+ ]
655
+ },
656
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
657
+ "version": "4.52.5",
658
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
659
+ "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
660
+ "cpu": [
661
+ "loong64"
662
+ ],
663
+ "dev": true,
664
+ "license": "MIT",
665
+ "optional": true,
666
+ "os": [
667
+ "linux"
668
+ ]
669
+ },
670
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
671
+ "version": "4.52.5",
672
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
673
+ "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
674
+ "cpu": [
675
+ "ppc64"
676
+ ],
677
+ "dev": true,
678
+ "license": "MIT",
679
+ "optional": true,
680
+ "os": [
681
+ "linux"
682
+ ]
683
+ },
684
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
685
+ "version": "4.52.5",
686
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
687
+ "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
688
+ "cpu": [
689
+ "riscv64"
690
+ ],
691
+ "dev": true,
692
+ "license": "MIT",
693
+ "optional": true,
694
+ "os": [
695
+ "linux"
696
+ ]
697
+ },
698
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
699
+ "version": "4.52.5",
700
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
701
+ "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
702
+ "cpu": [
703
+ "riscv64"
704
+ ],
705
+ "dev": true,
706
+ "license": "MIT",
707
+ "optional": true,
708
+ "os": [
709
+ "linux"
710
+ ]
711
+ },
712
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
713
+ "version": "4.52.5",
714
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
715
+ "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
716
+ "cpu": [
717
+ "s390x"
718
+ ],
719
+ "dev": true,
720
+ "license": "MIT",
721
+ "optional": true,
722
+ "os": [
723
+ "linux"
724
+ ]
725
+ },
726
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
727
+ "version": "4.52.5",
728
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
729
+ "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
730
+ "cpu": [
731
+ "x64"
732
+ ],
733
+ "dev": true,
734
+ "license": "MIT",
735
+ "optional": true,
736
+ "os": [
737
+ "linux"
738
+ ]
739
+ },
740
+ "node_modules/@rollup/rollup-linux-x64-musl": {
741
+ "version": "4.52.5",
742
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
743
+ "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
744
+ "cpu": [
745
+ "x64"
746
+ ],
747
+ "dev": true,
748
+ "license": "MIT",
749
+ "optional": true,
750
+ "os": [
751
+ "linux"
752
+ ]
753
+ },
754
+ "node_modules/@rollup/rollup-openharmony-arm64": {
755
+ "version": "4.52.5",
756
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
757
+ "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
758
+ "cpu": [
759
+ "arm64"
760
+ ],
761
+ "dev": true,
762
+ "license": "MIT",
763
+ "optional": true,
764
+ "os": [
765
+ "openharmony"
766
+ ]
767
+ },
768
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
769
+ "version": "4.52.5",
770
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
771
+ "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
772
+ "cpu": [
773
+ "arm64"
774
+ ],
775
+ "dev": true,
776
+ "license": "MIT",
777
+ "optional": true,
778
+ "os": [
779
+ "win32"
780
+ ]
781
+ },
782
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
783
+ "version": "4.52.5",
784
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
785
+ "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
786
+ "cpu": [
787
+ "ia32"
788
+ ],
789
+ "dev": true,
790
+ "license": "MIT",
791
+ "optional": true,
792
+ "os": [
793
+ "win32"
794
+ ]
795
+ },
796
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
797
+ "version": "4.52.5",
798
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
799
+ "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
800
+ "cpu": [
801
+ "x64"
802
+ ],
803
+ "dev": true,
804
+ "license": "MIT",
805
+ "optional": true,
806
+ "os": [
807
+ "win32"
808
+ ]
809
+ },
810
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
811
+ "version": "4.52.5",
812
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
813
+ "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
814
+ "cpu": [
815
+ "x64"
816
+ ],
817
+ "dev": true,
818
+ "license": "MIT",
819
+ "optional": true,
820
+ "os": [
821
+ "win32"
822
+ ]
823
+ },
824
+ "node_modules/@types/estree": {
825
+ "version": "1.0.8",
826
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
827
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
828
+ "dev": true,
829
+ "license": "MIT"
830
+ },
831
+ "node_modules/@types/node": {
832
+ "version": "22.18.12",
833
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz",
834
+ "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==",
835
+ "dev": true,
836
+ "license": "MIT",
837
+ "peer": true,
838
+ "dependencies": {
839
+ "undici-types": "~6.21.0"
840
+ }
841
+ },
842
+ "node_modules/@vitejs/plugin-vue": {
843
+ "version": "5.2.4",
844
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
845
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
846
+ "dev": true,
847
+ "license": "MIT",
848
+ "engines": {
849
+ "node": "^18.0.0 || >=20.0.0"
850
+ },
851
+ "peerDependencies": {
852
+ "vite": "^5.0.0 || ^6.0.0",
853
+ "vue": "^3.2.25"
854
+ }
855
+ },
856
+ "node_modules/@volar/language-core": {
857
+ "version": "2.4.15",
858
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
859
+ "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
860
+ "dev": true,
861
+ "license": "MIT",
862
+ "dependencies": {
863
+ "@volar/source-map": "2.4.15"
864
+ }
865
+ },
866
+ "node_modules/@volar/source-map": {
867
+ "version": "2.4.15",
868
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
869
+ "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
870
+ "dev": true,
871
+ "license": "MIT"
872
+ },
873
+ "node_modules/@volar/typescript": {
874
+ "version": "2.4.15",
875
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
876
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
877
+ "dev": true,
878
+ "license": "MIT",
879
+ "dependencies": {
880
+ "@volar/language-core": "2.4.15",
881
+ "path-browserify": "^1.0.1",
882
+ "vscode-uri": "^3.0.8"
883
+ }
884
+ },
885
+ "node_modules/@vue/compiler-core": {
886
+ "version": "3.5.22",
887
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
888
+ "integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
889
+ "license": "MIT",
890
+ "dependencies": {
891
+ "@babel/parser": "^7.28.4",
892
+ "@vue/shared": "3.5.22",
893
+ "entities": "^4.5.0",
894
+ "estree-walker": "^2.0.2",
895
+ "source-map-js": "^1.2.1"
896
+ }
897
+ },
898
+ "node_modules/@vue/compiler-dom": {
899
+ "version": "3.5.22",
900
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
901
+ "integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
902
+ "license": "MIT",
903
+ "dependencies": {
904
+ "@vue/compiler-core": "3.5.22",
905
+ "@vue/shared": "3.5.22"
906
+ }
907
+ },
908
+ "node_modules/@vue/compiler-sfc": {
909
+ "version": "3.5.22",
910
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
911
+ "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
912
+ "license": "MIT",
913
+ "dependencies": {
914
+ "@babel/parser": "^7.28.4",
915
+ "@vue/compiler-core": "3.5.22",
916
+ "@vue/compiler-dom": "3.5.22",
917
+ "@vue/compiler-ssr": "3.5.22",
918
+ "@vue/shared": "3.5.22",
919
+ "estree-walker": "^2.0.2",
920
+ "magic-string": "^0.30.19",
921
+ "postcss": "^8.5.6",
922
+ "source-map-js": "^1.2.1"
923
+ }
924
+ },
925
+ "node_modules/@vue/compiler-ssr": {
926
+ "version": "3.5.22",
927
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
928
+ "integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
929
+ "license": "MIT",
930
+ "dependencies": {
931
+ "@vue/compiler-dom": "3.5.22",
932
+ "@vue/shared": "3.5.22"
933
+ }
934
+ },
935
+ "node_modules/@vue/compiler-vue2": {
936
+ "version": "2.7.16",
937
+ "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
938
+ "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
939
+ "dev": true,
940
+ "license": "MIT",
941
+ "dependencies": {
942
+ "de-indent": "^1.0.2",
943
+ "he": "^1.2.0"
944
+ }
945
+ },
946
+ "node_modules/@vue/language-core": {
947
+ "version": "2.2.12",
948
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
949
+ "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
950
+ "dev": true,
951
+ "license": "MIT",
952
+ "dependencies": {
953
+ "@volar/language-core": "2.4.15",
954
+ "@vue/compiler-dom": "^3.5.0",
955
+ "@vue/compiler-vue2": "^2.7.16",
956
+ "@vue/shared": "^3.5.0",
957
+ "alien-signals": "^1.0.3",
958
+ "minimatch": "^9.0.3",
959
+ "muggle-string": "^0.4.1",
960
+ "path-browserify": "^1.0.1"
961
+ },
962
+ "peerDependencies": {
963
+ "typescript": "*"
964
+ },
965
+ "peerDependenciesMeta": {
966
+ "typescript": {
967
+ "optional": true
968
+ }
969
+ }
970
+ },
971
+ "node_modules/@vue/reactivity": {
972
+ "version": "3.5.22",
973
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
974
+ "integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
975
+ "license": "MIT",
976
+ "dependencies": {
977
+ "@vue/shared": "3.5.22"
978
+ }
979
+ },
980
+ "node_modules/@vue/runtime-core": {
981
+ "version": "3.5.22",
982
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
983
+ "integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
984
+ "license": "MIT",
985
+ "dependencies": {
986
+ "@vue/reactivity": "3.5.22",
987
+ "@vue/shared": "3.5.22"
988
+ }
989
+ },
990
+ "node_modules/@vue/runtime-dom": {
991
+ "version": "3.5.22",
992
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
993
+ "integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
994
+ "license": "MIT",
995
+ "dependencies": {
996
+ "@vue/reactivity": "3.5.22",
997
+ "@vue/runtime-core": "3.5.22",
998
+ "@vue/shared": "3.5.22",
999
+ "csstype": "^3.1.3"
1000
+ }
1001
+ },
1002
+ "node_modules/@vue/server-renderer": {
1003
+ "version": "3.5.22",
1004
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
1005
+ "integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
1006
+ "license": "MIT",
1007
+ "dependencies": {
1008
+ "@vue/compiler-ssr": "3.5.22",
1009
+ "@vue/shared": "3.5.22"
1010
+ },
1011
+ "peerDependencies": {
1012
+ "vue": "3.5.22"
1013
+ }
1014
+ },
1015
+ "node_modules/@vue/shared": {
1016
+ "version": "3.5.22",
1017
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz",
1018
+ "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
1019
+ "license": "MIT"
1020
+ },
1021
+ "node_modules/alien-signals": {
1022
+ "version": "1.0.13",
1023
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
1024
+ "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
1025
+ "dev": true,
1026
+ "license": "MIT"
1027
+ },
1028
+ "node_modules/asynckit": {
1029
+ "version": "0.4.0",
1030
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
1031
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
1032
+ "license": "MIT"
1033
+ },
1034
+ "node_modules/axios": {
1035
+ "version": "1.12.2",
1036
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
1037
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
1038
+ "license": "MIT",
1039
+ "dependencies": {
1040
+ "follow-redirects": "^1.15.6",
1041
+ "form-data": "^4.0.4",
1042
+ "proxy-from-env": "^1.1.0"
1043
+ }
1044
+ },
1045
+ "node_modules/balanced-match": {
1046
+ "version": "1.0.2",
1047
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
1048
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
1049
+ "dev": true,
1050
+ "license": "MIT"
1051
+ },
1052
+ "node_modules/brace-expansion": {
1053
+ "version": "2.0.2",
1054
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
1055
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
1056
+ "dev": true,
1057
+ "license": "MIT",
1058
+ "dependencies": {
1059
+ "balanced-match": "^1.0.0"
1060
+ }
1061
+ },
1062
+ "node_modules/call-bind-apply-helpers": {
1063
+ "version": "1.0.2",
1064
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
1065
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
1066
+ "license": "MIT",
1067
+ "dependencies": {
1068
+ "es-errors": "^1.3.0",
1069
+ "function-bind": "^1.1.2"
1070
+ },
1071
+ "engines": {
1072
+ "node": ">= 0.4"
1073
+ }
1074
+ },
1075
+ "node_modules/combined-stream": {
1076
+ "version": "1.0.8",
1077
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
1078
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
1079
+ "license": "MIT",
1080
+ "dependencies": {
1081
+ "delayed-stream": "~1.0.0"
1082
+ },
1083
+ "engines": {
1084
+ "node": ">= 0.8"
1085
+ }
1086
+ },
1087
+ "node_modules/csstype": {
1088
+ "version": "3.1.3",
1089
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
1090
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
1091
+ "license": "MIT"
1092
+ },
1093
+ "node_modules/de-indent": {
1094
+ "version": "1.0.2",
1095
+ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
1096
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
1097
+ "dev": true,
1098
+ "license": "MIT"
1099
+ },
1100
+ "node_modules/delayed-stream": {
1101
+ "version": "1.0.0",
1102
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
1103
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
1104
+ "license": "MIT",
1105
+ "engines": {
1106
+ "node": ">=0.4.0"
1107
+ }
1108
+ },
1109
+ "node_modules/dunder-proto": {
1110
+ "version": "1.0.1",
1111
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
1112
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
1113
+ "license": "MIT",
1114
+ "dependencies": {
1115
+ "call-bind-apply-helpers": "^1.0.1",
1116
+ "es-errors": "^1.3.0",
1117
+ "gopd": "^1.2.0"
1118
+ },
1119
+ "engines": {
1120
+ "node": ">= 0.4"
1121
+ }
1122
+ },
1123
+ "node_modules/entities": {
1124
+ "version": "4.5.0",
1125
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
1126
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
1127
+ "license": "BSD-2-Clause",
1128
+ "engines": {
1129
+ "node": ">=0.12"
1130
+ },
1131
+ "funding": {
1132
+ "url": "https://github.com/fb55/entities?sponsor=1"
1133
+ }
1134
+ },
1135
+ "node_modules/es-define-property": {
1136
+ "version": "1.0.1",
1137
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1138
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1139
+ "license": "MIT",
1140
+ "engines": {
1141
+ "node": ">= 0.4"
1142
+ }
1143
+ },
1144
+ "node_modules/es-errors": {
1145
+ "version": "1.3.0",
1146
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1147
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1148
+ "license": "MIT",
1149
+ "engines": {
1150
+ "node": ">= 0.4"
1151
+ }
1152
+ },
1153
+ "node_modules/es-object-atoms": {
1154
+ "version": "1.1.1",
1155
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
1156
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
1157
+ "license": "MIT",
1158
+ "dependencies": {
1159
+ "es-errors": "^1.3.0"
1160
+ },
1161
+ "engines": {
1162
+ "node": ">= 0.4"
1163
+ }
1164
+ },
1165
+ "node_modules/es-set-tostringtag": {
1166
+ "version": "2.1.0",
1167
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
1168
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
1169
+ "license": "MIT",
1170
+ "dependencies": {
1171
+ "es-errors": "^1.3.0",
1172
+ "get-intrinsic": "^1.2.6",
1173
+ "has-tostringtag": "^1.0.2",
1174
+ "hasown": "^2.0.2"
1175
+ },
1176
+ "engines": {
1177
+ "node": ">= 0.4"
1178
+ }
1179
+ },
1180
+ "node_modules/esbuild": {
1181
+ "version": "0.25.11",
1182
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
1183
+ "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==",
1184
+ "dev": true,
1185
+ "hasInstallScript": true,
1186
+ "license": "MIT",
1187
+ "bin": {
1188
+ "esbuild": "bin/esbuild"
1189
+ },
1190
+ "engines": {
1191
+ "node": ">=18"
1192
+ },
1193
+ "optionalDependencies": {
1194
+ "@esbuild/aix-ppc64": "0.25.11",
1195
+ "@esbuild/android-arm": "0.25.11",
1196
+ "@esbuild/android-arm64": "0.25.11",
1197
+ "@esbuild/android-x64": "0.25.11",
1198
+ "@esbuild/darwin-arm64": "0.25.11",
1199
+ "@esbuild/darwin-x64": "0.25.11",
1200
+ "@esbuild/freebsd-arm64": "0.25.11",
1201
+ "@esbuild/freebsd-x64": "0.25.11",
1202
+ "@esbuild/linux-arm": "0.25.11",
1203
+ "@esbuild/linux-arm64": "0.25.11",
1204
+ "@esbuild/linux-ia32": "0.25.11",
1205
+ "@esbuild/linux-loong64": "0.25.11",
1206
+ "@esbuild/linux-mips64el": "0.25.11",
1207
+ "@esbuild/linux-ppc64": "0.25.11",
1208
+ "@esbuild/linux-riscv64": "0.25.11",
1209
+ "@esbuild/linux-s390x": "0.25.11",
1210
+ "@esbuild/linux-x64": "0.25.11",
1211
+ "@esbuild/netbsd-arm64": "0.25.11",
1212
+ "@esbuild/netbsd-x64": "0.25.11",
1213
+ "@esbuild/openbsd-arm64": "0.25.11",
1214
+ "@esbuild/openbsd-x64": "0.25.11",
1215
+ "@esbuild/openharmony-arm64": "0.25.11",
1216
+ "@esbuild/sunos-x64": "0.25.11",
1217
+ "@esbuild/win32-arm64": "0.25.11",
1218
+ "@esbuild/win32-ia32": "0.25.11",
1219
+ "@esbuild/win32-x64": "0.25.11"
1220
+ }
1221
+ },
1222
+ "node_modules/estree-walker": {
1223
+ "version": "2.0.2",
1224
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
1225
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
1226
+ "license": "MIT"
1227
+ },
1228
+ "node_modules/fdir": {
1229
+ "version": "6.5.0",
1230
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1231
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1232
+ "dev": true,
1233
+ "license": "MIT",
1234
+ "engines": {
1235
+ "node": ">=12.0.0"
1236
+ },
1237
+ "peerDependencies": {
1238
+ "picomatch": "^3 || ^4"
1239
+ },
1240
+ "peerDependenciesMeta": {
1241
+ "picomatch": {
1242
+ "optional": true
1243
+ }
1244
+ }
1245
+ },
1246
+ "node_modules/follow-redirects": {
1247
+ "version": "1.15.11",
1248
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
1249
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
1250
+ "funding": [
1251
+ {
1252
+ "type": "individual",
1253
+ "url": "https://github.com/sponsors/RubenVerborgh"
1254
+ }
1255
+ ],
1256
+ "license": "MIT",
1257
+ "engines": {
1258
+ "node": ">=4.0"
1259
+ },
1260
+ "peerDependenciesMeta": {
1261
+ "debug": {
1262
+ "optional": true
1263
+ }
1264
+ }
1265
+ },
1266
+ "node_modules/form-data": {
1267
+ "version": "4.0.4",
1268
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
1269
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
1270
+ "license": "MIT",
1271
+ "dependencies": {
1272
+ "asynckit": "^0.4.0",
1273
+ "combined-stream": "^1.0.8",
1274
+ "es-set-tostringtag": "^2.1.0",
1275
+ "hasown": "^2.0.2",
1276
+ "mime-types": "^2.1.12"
1277
+ },
1278
+ "engines": {
1279
+ "node": ">= 6"
1280
+ }
1281
+ },
1282
+ "node_modules/fsevents": {
1283
+ "version": "2.3.3",
1284
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1285
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1286
+ "dev": true,
1287
+ "hasInstallScript": true,
1288
+ "license": "MIT",
1289
+ "optional": true,
1290
+ "os": [
1291
+ "darwin"
1292
+ ],
1293
+ "engines": {
1294
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1295
+ }
1296
+ },
1297
+ "node_modules/function-bind": {
1298
+ "version": "1.1.2",
1299
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1300
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1301
+ "license": "MIT",
1302
+ "funding": {
1303
+ "url": "https://github.com/sponsors/ljharb"
1304
+ }
1305
+ },
1306
+ "node_modules/get-intrinsic": {
1307
+ "version": "1.3.0",
1308
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1309
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1310
+ "license": "MIT",
1311
+ "dependencies": {
1312
+ "call-bind-apply-helpers": "^1.0.2",
1313
+ "es-define-property": "^1.0.1",
1314
+ "es-errors": "^1.3.0",
1315
+ "es-object-atoms": "^1.1.1",
1316
+ "function-bind": "^1.1.2",
1317
+ "get-proto": "^1.0.1",
1318
+ "gopd": "^1.2.0",
1319
+ "has-symbols": "^1.1.0",
1320
+ "hasown": "^2.0.2",
1321
+ "math-intrinsics": "^1.1.0"
1322
+ },
1323
+ "engines": {
1324
+ "node": ">= 0.4"
1325
+ },
1326
+ "funding": {
1327
+ "url": "https://github.com/sponsors/ljharb"
1328
+ }
1329
+ },
1330
+ "node_modules/get-proto": {
1331
+ "version": "1.0.1",
1332
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1333
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1334
+ "license": "MIT",
1335
+ "dependencies": {
1336
+ "dunder-proto": "^1.0.1",
1337
+ "es-object-atoms": "^1.0.0"
1338
+ },
1339
+ "engines": {
1340
+ "node": ">= 0.4"
1341
+ }
1342
+ },
1343
+ "node_modules/gopd": {
1344
+ "version": "1.2.0",
1345
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1346
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1347
+ "license": "MIT",
1348
+ "engines": {
1349
+ "node": ">= 0.4"
1350
+ },
1351
+ "funding": {
1352
+ "url": "https://github.com/sponsors/ljharb"
1353
+ }
1354
+ },
1355
+ "node_modules/has-symbols": {
1356
+ "version": "1.1.0",
1357
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1358
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1359
+ "license": "MIT",
1360
+ "engines": {
1361
+ "node": ">= 0.4"
1362
+ },
1363
+ "funding": {
1364
+ "url": "https://github.com/sponsors/ljharb"
1365
+ }
1366
+ },
1367
+ "node_modules/has-tostringtag": {
1368
+ "version": "1.0.2",
1369
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
1370
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
1371
+ "license": "MIT",
1372
+ "dependencies": {
1373
+ "has-symbols": "^1.0.3"
1374
+ },
1375
+ "engines": {
1376
+ "node": ">= 0.4"
1377
+ },
1378
+ "funding": {
1379
+ "url": "https://github.com/sponsors/ljharb"
1380
+ }
1381
+ },
1382
+ "node_modules/hasown": {
1383
+ "version": "2.0.2",
1384
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1385
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1386
+ "license": "MIT",
1387
+ "dependencies": {
1388
+ "function-bind": "^1.1.2"
1389
+ },
1390
+ "engines": {
1391
+ "node": ">= 0.4"
1392
+ }
1393
+ },
1394
+ "node_modules/he": {
1395
+ "version": "1.2.0",
1396
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
1397
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
1398
+ "dev": true,
1399
+ "license": "MIT",
1400
+ "bin": {
1401
+ "he": "bin/he"
1402
+ }
1403
+ },
1404
+ "node_modules/magic-string": {
1405
+ "version": "0.30.19",
1406
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
1407
+ "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
1408
+ "license": "MIT",
1409
+ "dependencies": {
1410
+ "@jridgewell/sourcemap-codec": "^1.5.5"
1411
+ }
1412
+ },
1413
+ "node_modules/math-intrinsics": {
1414
+ "version": "1.1.0",
1415
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1416
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1417
+ "license": "MIT",
1418
+ "engines": {
1419
+ "node": ">= 0.4"
1420
+ }
1421
+ },
1422
+ "node_modules/mime-db": {
1423
+ "version": "1.52.0",
1424
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1425
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1426
+ "license": "MIT",
1427
+ "engines": {
1428
+ "node": ">= 0.6"
1429
+ }
1430
+ },
1431
+ "node_modules/mime-types": {
1432
+ "version": "2.1.35",
1433
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1434
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1435
+ "license": "MIT",
1436
+ "dependencies": {
1437
+ "mime-db": "1.52.0"
1438
+ },
1439
+ "engines": {
1440
+ "node": ">= 0.6"
1441
+ }
1442
+ },
1443
+ "node_modules/minimatch": {
1444
+ "version": "9.0.5",
1445
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
1446
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
1447
+ "dev": true,
1448
+ "license": "ISC",
1449
+ "dependencies": {
1450
+ "brace-expansion": "^2.0.1"
1451
+ },
1452
+ "engines": {
1453
+ "node": ">=16 || 14 >=14.17"
1454
+ },
1455
+ "funding": {
1456
+ "url": "https://github.com/sponsors/isaacs"
1457
+ }
1458
+ },
1459
+ "node_modules/muggle-string": {
1460
+ "version": "0.4.1",
1461
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
1462
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
1463
+ "dev": true,
1464
+ "license": "MIT"
1465
+ },
1466
+ "node_modules/nanoid": {
1467
+ "version": "3.3.11",
1468
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1469
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1470
+ "funding": [
1471
+ {
1472
+ "type": "github",
1473
+ "url": "https://github.com/sponsors/ai"
1474
+ }
1475
+ ],
1476
+ "license": "MIT",
1477
+ "bin": {
1478
+ "nanoid": "bin/nanoid.cjs"
1479
+ },
1480
+ "engines": {
1481
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1482
+ }
1483
+ },
1484
+ "node_modules/path-browserify": {
1485
+ "version": "1.0.1",
1486
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
1487
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
1488
+ "dev": true,
1489
+ "license": "MIT"
1490
+ },
1491
+ "node_modules/picocolors": {
1492
+ "version": "1.1.1",
1493
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1494
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1495
+ "license": "ISC"
1496
+ },
1497
+ "node_modules/picomatch": {
1498
+ "version": "4.0.3",
1499
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1500
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1501
+ "dev": true,
1502
+ "license": "MIT",
1503
+ "peer": true,
1504
+ "engines": {
1505
+ "node": ">=12"
1506
+ },
1507
+ "funding": {
1508
+ "url": "https://github.com/sponsors/jonschlinkert"
1509
+ }
1510
+ },
1511
+ "node_modules/postcss": {
1512
+ "version": "8.5.6",
1513
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
1514
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
1515
+ "funding": [
1516
+ {
1517
+ "type": "opencollective",
1518
+ "url": "https://opencollective.com/postcss/"
1519
+ },
1520
+ {
1521
+ "type": "tidelift",
1522
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1523
+ },
1524
+ {
1525
+ "type": "github",
1526
+ "url": "https://github.com/sponsors/ai"
1527
+ }
1528
+ ],
1529
+ "license": "MIT",
1530
+ "dependencies": {
1531
+ "nanoid": "^3.3.11",
1532
+ "picocolors": "^1.1.1",
1533
+ "source-map-js": "^1.2.1"
1534
+ },
1535
+ "engines": {
1536
+ "node": "^10 || ^12 || >=14"
1537
+ }
1538
+ },
1539
+ "node_modules/proxy-from-env": {
1540
+ "version": "1.1.0",
1541
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1542
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
1543
+ "license": "MIT"
1544
+ },
1545
+ "node_modules/rollup": {
1546
+ "version": "4.52.5",
1547
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
1548
+ "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
1549
+ "dev": true,
1550
+ "license": "MIT",
1551
+ "dependencies": {
1552
+ "@types/estree": "1.0.8"
1553
+ },
1554
+ "bin": {
1555
+ "rollup": "dist/bin/rollup"
1556
+ },
1557
+ "engines": {
1558
+ "node": ">=18.0.0",
1559
+ "npm": ">=8.0.0"
1560
+ },
1561
+ "optionalDependencies": {
1562
+ "@rollup/rollup-android-arm-eabi": "4.52.5",
1563
+ "@rollup/rollup-android-arm64": "4.52.5",
1564
+ "@rollup/rollup-darwin-arm64": "4.52.5",
1565
+ "@rollup/rollup-darwin-x64": "4.52.5",
1566
+ "@rollup/rollup-freebsd-arm64": "4.52.5",
1567
+ "@rollup/rollup-freebsd-x64": "4.52.5",
1568
+ "@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
1569
+ "@rollup/rollup-linux-arm-musleabihf": "4.52.5",
1570
+ "@rollup/rollup-linux-arm64-gnu": "4.52.5",
1571
+ "@rollup/rollup-linux-arm64-musl": "4.52.5",
1572
+ "@rollup/rollup-linux-loong64-gnu": "4.52.5",
1573
+ "@rollup/rollup-linux-ppc64-gnu": "4.52.5",
1574
+ "@rollup/rollup-linux-riscv64-gnu": "4.52.5",
1575
+ "@rollup/rollup-linux-riscv64-musl": "4.52.5",
1576
+ "@rollup/rollup-linux-s390x-gnu": "4.52.5",
1577
+ "@rollup/rollup-linux-x64-gnu": "4.52.5",
1578
+ "@rollup/rollup-linux-x64-musl": "4.52.5",
1579
+ "@rollup/rollup-openharmony-arm64": "4.52.5",
1580
+ "@rollup/rollup-win32-arm64-msvc": "4.52.5",
1581
+ "@rollup/rollup-win32-ia32-msvc": "4.52.5",
1582
+ "@rollup/rollup-win32-x64-gnu": "4.52.5",
1583
+ "@rollup/rollup-win32-x64-msvc": "4.52.5",
1584
+ "fsevents": "~2.3.2"
1585
+ }
1586
+ },
1587
+ "node_modules/source-map-js": {
1588
+ "version": "1.2.1",
1589
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1590
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1591
+ "license": "BSD-3-Clause",
1592
+ "engines": {
1593
+ "node": ">=0.10.0"
1594
+ }
1595
+ },
1596
+ "node_modules/tinyglobby": {
1597
+ "version": "0.2.15",
1598
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
1599
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
1600
+ "dev": true,
1601
+ "license": "MIT",
1602
+ "dependencies": {
1603
+ "fdir": "^6.5.0",
1604
+ "picomatch": "^4.0.3"
1605
+ },
1606
+ "engines": {
1607
+ "node": ">=12.0.0"
1608
+ },
1609
+ "funding": {
1610
+ "url": "https://github.com/sponsors/SuperchupuDev"
1611
+ }
1612
+ },
1613
+ "node_modules/typescript": {
1614
+ "version": "5.9.3",
1615
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1616
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1617
+ "devOptional": true,
1618
+ "license": "Apache-2.0",
1619
+ "peer": true,
1620
+ "bin": {
1621
+ "tsc": "bin/tsc",
1622
+ "tsserver": "bin/tsserver"
1623
+ },
1624
+ "engines": {
1625
+ "node": ">=14.17"
1626
+ }
1627
+ },
1628
+ "node_modules/undici-types": {
1629
+ "version": "6.21.0",
1630
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
1631
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
1632
+ "dev": true,
1633
+ "license": "MIT"
1634
+ },
1635
+ "node_modules/vite": {
1636
+ "version": "6.4.1",
1637
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
1638
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
1639
+ "dev": true,
1640
+ "license": "MIT",
1641
+ "peer": true,
1642
+ "dependencies": {
1643
+ "esbuild": "^0.25.0",
1644
+ "fdir": "^6.4.4",
1645
+ "picomatch": "^4.0.2",
1646
+ "postcss": "^8.5.3",
1647
+ "rollup": "^4.34.9",
1648
+ "tinyglobby": "^0.2.13"
1649
+ },
1650
+ "bin": {
1651
+ "vite": "bin/vite.js"
1652
+ },
1653
+ "engines": {
1654
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
1655
+ },
1656
+ "funding": {
1657
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1658
+ },
1659
+ "optionalDependencies": {
1660
+ "fsevents": "~2.3.3"
1661
+ },
1662
+ "peerDependencies": {
1663
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
1664
+ "jiti": ">=1.21.0",
1665
+ "less": "*",
1666
+ "lightningcss": "^1.21.0",
1667
+ "sass": "*",
1668
+ "sass-embedded": "*",
1669
+ "stylus": "*",
1670
+ "sugarss": "*",
1671
+ "terser": "^5.16.0",
1672
+ "tsx": "^4.8.1",
1673
+ "yaml": "^2.4.2"
1674
+ },
1675
+ "peerDependenciesMeta": {
1676
+ "@types/node": {
1677
+ "optional": true
1678
+ },
1679
+ "jiti": {
1680
+ "optional": true
1681
+ },
1682
+ "less": {
1683
+ "optional": true
1684
+ },
1685
+ "lightningcss": {
1686
+ "optional": true
1687
+ },
1688
+ "sass": {
1689
+ "optional": true
1690
+ },
1691
+ "sass-embedded": {
1692
+ "optional": true
1693
+ },
1694
+ "stylus": {
1695
+ "optional": true
1696
+ },
1697
+ "sugarss": {
1698
+ "optional": true
1699
+ },
1700
+ "terser": {
1701
+ "optional": true
1702
+ },
1703
+ "tsx": {
1704
+ "optional": true
1705
+ },
1706
+ "yaml": {
1707
+ "optional": true
1708
+ }
1709
+ }
1710
+ },
1711
+ "node_modules/vscode-uri": {
1712
+ "version": "3.1.0",
1713
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
1714
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
1715
+ "dev": true,
1716
+ "license": "MIT"
1717
+ },
1718
+ "node_modules/vue": {
1719
+ "version": "3.5.22",
1720
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
1721
+ "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
1722
+ "license": "MIT",
1723
+ "peer": true,
1724
+ "dependencies": {
1725
+ "@vue/compiler-dom": "3.5.22",
1726
+ "@vue/compiler-sfc": "3.5.22",
1727
+ "@vue/runtime-dom": "3.5.22",
1728
+ "@vue/server-renderer": "3.5.22",
1729
+ "@vue/shared": "3.5.22"
1730
+ },
1731
+ "peerDependencies": {
1732
+ "typescript": "*"
1733
+ },
1734
+ "peerDependenciesMeta": {
1735
+ "typescript": {
1736
+ "optional": true
1737
+ }
1738
+ }
1739
+ },
1740
+ "node_modules/vue-tsc": {
1741
+ "version": "2.2.12",
1742
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
1743
+ "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
1744
+ "dev": true,
1745
+ "license": "MIT",
1746
+ "dependencies": {
1747
+ "@volar/typescript": "2.4.15",
1748
+ "@vue/language-core": "2.2.12"
1749
+ },
1750
+ "bin": {
1751
+ "vue-tsc": "bin/vue-tsc.js"
1752
+ },
1753
+ "peerDependencies": {
1754
+ "typescript": ">=5.0.0"
1755
+ }
1756
+ }
1757
+ }
1758
+ }
frontend/package.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "helloagents-deepresearch-frontend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vue-tsc --noEmit && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "axios": "^1.7.9",
13
+ "vue": "^3.5.13"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^22.10.5",
17
+ "@vitejs/plugin-vue": "^5.2.1",
18
+ "typescript": "^5.7.3",
19
+ "vite": "^6.0.7",
20
+ "vue-tsc": "^2.2.0"
21
+ }
22
+ }
frontend/src/App.vue ADDED
@@ -0,0 +1,2304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <main class="app-shell" :class="{ expanded: isExpanded }">
3
+ <div class="aurora" aria-hidden="true">
4
+ <span></span>
5
+ <span></span>
6
+ <span></span>
7
+ </div>
8
+
9
+ <!-- Initial state: centered input card -->
10
+ <div v-if="!isExpanded" class="layout layout-centered">
11
+ <section class="panel panel-form panel-centered">
12
+ <header class="panel-head">
13
+ <div class="logo">
14
+ <svg viewBox="0 0 24 24" aria-hidden="true">
15
+ <path
16
+ d="M12 2.5c-.7 0-1.4.2-2 .6L4.6 7C3.6 7.6 3 8.7 3 9.9v4.2c0 1.2.6 2.3 1.6 2.9l5.4 3.9c1.2.8 2.8.8 4 0l5.4-3.9c1-.7 1.6-1.7 1.6-2.9V9.9c0-1.2-.6-2.3-1.6-2.9L14 3.1a3.6 3.6 0 0 0-2-.6Z"
17
+ />
18
+ </svg>
19
+ </div>
20
+ <div>
21
+ <h1>Deep Research Assistant</h1>
22
+ <p>Combining multi-round intelligent search and summarization, presenting insights and citations in real-time.</p>
23
+ </div>
24
+ </header>
25
+
26
+ <form class="form" @submit.prevent="handleSubmit">
27
+ <label class="field">
28
+ <span>Research Topic</span>
29
+ <textarea
30
+ v-model="form.topic"
31
+ placeholder="e.g., Explore key breakthroughs in multimodal models in 2025"
32
+ rows="4"
33
+ required
34
+ ></textarea>
35
+ </label>
36
+
37
+ <section class="options">
38
+ <label class="field option">
39
+ <span>Search Engine</span>
40
+ <select v-model="form.searchApi">
41
+ <option value="">Use backend default</option>
42
+ <option
43
+ v-for="option in searchOptions"
44
+ :key="option"
45
+ :value="option"
46
+ >
47
+ {{ option }}
48
+ </option>
49
+ </select>
50
+ </label>
51
+ </section>
52
+
53
+ <div class="form-actions">
54
+ <button class="submit" type="submit" :disabled="loading">
55
+ <span class="submit-label">
56
+ <svg
57
+ v-if="loading"
58
+ class="spinner"
59
+ viewBox="0 0 24 24"
60
+ aria-hidden="true"
61
+ >
62
+ <circle cx="12" cy="12" r="9" stroke-width="3" />
63
+ </svg>
64
+ {{ loading ? "Research in progress..." : "Start Research" }}
65
+ </span>
66
+ </button>
67
+ <button
68
+ v-if="loading"
69
+ type="button"
70
+ class="secondary-btn"
71
+ @click="cancelResearch"
72
+ >
73
+ Cancel Research
74
+ </button>
75
+ </div>
76
+ </form>
77
+
78
+ <p v-if="error" class="error-chip">
79
+ <svg viewBox="0 0 20 20" aria-hidden="true">
80
+ <path
81
+ d="M10 3.2c-.3 0-.6.2-.8.5L3.4 15c-.4.7.1 1.6.8 1.6h11.6c.7 0 1.2-.9.8-1.6L10.8 3.7c-.2-.3-.5-.5-.8-.5Zm0 4.3c.4 0 .7.3.7.7v4c0 .4-.3.7-.7.7s-.7-.3-.7-.7V8.2c0-.4.3-.7.7-.7Zm0 6.6a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"
82
+ />
83
+ </svg>
84
+ {{ error }}
85
+ </p>
86
+ <p v-else-if="loading" class="hint muted">
87
+ Collecting clues and evidence, real-time progress shown on the right.
88
+ </p>
89
+ </section>
90
+ </div>
91
+
92
+ <!-- Fullscreen state: left-right split layout -->
93
+ <div v-else class="layout layout-fullscreen">
94
+ <!-- Left side: Research info -->
95
+ <aside class="sidebar">
96
+ <div class="sidebar-header">
97
+ <button class="back-btn" @click="goBack" :disabled="loading">
98
+ <svg viewBox="0 0 24 24" width="20" height="20">
99
+ <path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
100
+ </svg>
101
+ Back
102
+ </button>
103
+ <h2>🔍 Deep Research Assistant</h2>
104
+ </div>
105
+
106
+ <div class="research-info">
107
+ <div class="info-item">
108
+ <label>Research Topic</label>
109
+ <p class="topic-display">{{ form.topic }}</p>
110
+ </div>
111
+
112
+ <div class="info-item" v-if="form.searchApi">
113
+ <label>Search Engine</label>
114
+ <p>{{ form.searchApi }}</p>
115
+ </div>
116
+
117
+ <div class="info-item" v-if="totalTasks > 0">
118
+ <label>Research Progress</label>
119
+ <div class="progress-bar">
120
+ <div class="progress-fill" :style="{ width: `${(completedTasks / totalTasks) * 100}%` }"></div>
121
+ </div>
122
+ <p class="progress-text">{{ completedTasks }} / {{ totalTasks }} tasks completed</p>
123
+ </div>
124
+ </div>
125
+
126
+ <div class="sidebar-actions">
127
+ <button class="new-research-btn" @click="startNewResearch">
128
+ <svg viewBox="0 0 24 24" width="18" height="18">
129
+ <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
130
+ </svg>
131
+ Start New Research
132
+ </button>
133
+ </div>
134
+ </aside>
135
+
136
+ <!-- Right side: Research results -->
137
+ <section
138
+ class="panel panel-result"
139
+ v-if="todoTasks.length || reportMarkdown || progressLogs.length"
140
+ >
141
+ <header class="status-bar">
142
+ <div class="status-main">
143
+ <div class="status-chip" :class="{ active: loading }">
144
+ <span class="dot"></span>
145
+ {{ loading ? "Research in progress" : "Research completed" }}
146
+ </div>
147
+ <span class="status-meta">
148
+ Task progress: {{ completedTasks }} / {{ totalTasks || todoTasks.length || 1 }}
149
+ · {{ progressLogs.length }} phase records
150
+ </span>
151
+ </div>
152
+ <div class="status-controls">
153
+ <button class="secondary-btn" @click="logsCollapsed = !logsCollapsed">
154
+ {{ logsCollapsed ? "Expand process" : "Collapse process" }}
155
+ </button>
156
+ </div>
157
+ </header>
158
+
159
+ <div class="timeline-wrapper" v-show="!logsCollapsed && progressLogs.length">
160
+ <transition-group name="timeline" tag="ul" class="timeline">
161
+ <li v-for="(log, index) in progressLogs" :key="`${log}-${index}`">
162
+ <span class="timeline-node"></span>
163
+ <p>{{ log }}</p>
164
+ </li>
165
+ </transition-group>
166
+ </div>
167
+
168
+ <div class="tasks-section" v-if="todoTasks.length">
169
+ <aside class="tasks-list">
170
+ <h3>Task List</h3>
171
+ <ul>
172
+ <li
173
+ v-for="task in todoTasks"
174
+ :key="task.id"
175
+ :class="['task-item', { active: task.id === activeTaskId, completed: task.status === 'completed' }]"
176
+ >
177
+ <button
178
+ type="button"
179
+ class="task-button"
180
+ @click="activeTaskId = task.id"
181
+ >
182
+ <span class="task-title">{{ task.title }}</span>
183
+ <span class="task-status" :class="task.status">
184
+ {{ formatTaskStatus(task.status) }}
185
+ </span>
186
+ </button>
187
+ <p class="task-intent">{{ task.intent }}</p>
188
+ </li>
189
+ </ul>
190
+ </aside>
191
+
192
+ <article class="task-detail" v-if="currentTask">
193
+ <header class="task-header">
194
+ <div>
195
+ <h3>{{ currentTaskTitle || "Current Task" }}</h3>
196
+ <p class="muted" v-if="currentTaskIntent">
197
+ {{ currentTaskIntent }}
198
+ </p>
199
+ </div>
200
+ <div class="task-chip-group">
201
+ <span class="task-label">Query: {{ currentTaskQuery || "" }}</span>
202
+ <span
203
+ v-if="currentTaskNoteId"
204
+ class="task-label note-chip"
205
+ :title="currentTaskNoteId"
206
+ >
207
+ Note: {{ currentTaskNoteId }}
208
+ </span>
209
+ <span
210
+ v-if="currentTaskNotePath"
211
+ class="task-label note-chip path-chip"
212
+ :title="currentTaskNotePath"
213
+ >
214
+ <span class="path-label">Path:</span>
215
+ <span class="path-text">{{ currentTaskNotePath }}</span>
216
+ <button
217
+ class="chip-action"
218
+ type="button"
219
+ @click="copyNotePath(currentTaskNotePath)"
220
+ >
221
+ Copy
222
+ </button>
223
+ </span>
224
+ </div>
225
+ </header>
226
+
227
+ <section v-if="currentTask && currentTask.notices.length" class="task-notices">
228
+ <h4>System Notices</h4>
229
+ <ul>
230
+ <li v-for="(notice, idx) in currentTask.notices" :key="`${notice}-${idx}`">
231
+ {{ notice }}
232
+ </li>
233
+ </ul>
234
+ </section>
235
+
236
+ <section
237
+ class="sources-block"
238
+ :class="{ 'block-highlight': sourcesHighlight }"
239
+ >
240
+ <h3>Latest Sources</h3>
241
+ <template v-if="currentTaskSources.length">
242
+ <ul class="sources-list">
243
+ <li
244
+ v-for="(item, index) in currentTaskSources"
245
+ :key="`${item.title}-${index}`"
246
+ class="source-item"
247
+ >
248
+ <a
249
+ class="source-link"
250
+ :href="item.url || '#'"
251
+ target="_blank"
252
+ rel="noopener noreferrer"
253
+ >
254
+ {{ item.title || item.url || `Source ${index + 1}` }}
255
+ </a>
256
+ <div v-if="item.snippet || item.raw" class="source-tooltip">
257
+ <p v-if="item.snippet">{{ item.snippet }}</p>
258
+ <p v-if="item.raw" class="muted-text">{{ item.raw }}</p>
259
+ </div>
260
+ </li>
261
+ </ul>
262
+ </template>
263
+ <p v-else class="muted">No sources available</p>
264
+ </section>
265
+
266
+ <section
267
+ class="summary-block"
268
+ :class="{ 'block-highlight': summaryHighlight }"
269
+ >
270
+ <h3>Task Summary</h3>
271
+ <pre class="block-pre">{{ currentTaskSummary || "No information available" }}</pre>
272
+ </section>
273
+
274
+ <section
275
+ class="tools-block"
276
+ :class="{ 'block-highlight': toolHighlight }"
277
+ v-if="currentTaskToolCalls.length"
278
+ >
279
+ <h3>Tool Call Records</h3>
280
+ <ul class="tool-list">
281
+ <li
282
+ v-for="entry in currentTaskToolCalls"
283
+ :key="`${entry.eventId}-${entry.timestamp}`"
284
+ class="tool-entry"
285
+ >
286
+ <div class="tool-entry-header">
287
+ <span class="tool-entry-title">
288
+ #{{ entry.eventId }} {{ entry.agent }} → {{ entry.tool }}
289
+ </span>
290
+ <span
291
+ v-if="entry.noteId"
292
+ class="tool-entry-note"
293
+ >
294
+ Note: {{ entry.noteId }}
295
+ </span>
296
+ </div>
297
+ <p v-if="entry.notePath" class="tool-entry-path">
298
+ Note path:
299
+ <button
300
+ class="link-btn"
301
+ type="button"
302
+ @click="copyNotePath(entry.notePath)"
303
+ >
304
+ Copy
305
+ </button>
306
+ <span class="path-text">{{ entry.notePath }}</span>
307
+ </p>
308
+ <p class="tool-subtitle">Parameters</p>
309
+ <pre class="tool-pre">{{ formatToolParameters(entry.parameters) }}</pre>
310
+ <template v-if="entry.result">
311
+ <p class="tool-subtitle">Execution Result</p>
312
+ <pre class="tool-pre">{{ formatToolResult(entry.result) }}</pre>
313
+ </template>
314
+ </li>
315
+ </ul>
316
+ </section>
317
+ </article>
318
+
319
+ <article class="task-detail" v-else>
320
+ <p class="muted">Waiting for task planning or execution results.</p>
321
+ </article>
322
+ </div>
323
+
324
+ <div
325
+ v-if="reportMarkdown"
326
+ class="report-block"
327
+ :class="{ 'block-highlight': reportHighlight }"
328
+ >
329
+ <h3>Final Report</h3>
330
+ <pre class="block-pre">{{ reportMarkdown }}</pre>
331
+ </div>
332
+ </section>
333
+
334
+ </div>
335
+ </main>
336
+ </template>
337
+
338
+ <script lang="ts" setup>
339
+ import { computed, onBeforeUnmount, reactive, ref } from "vue";
340
+
341
+ import {
342
+ runResearchStream,
343
+ type ResearchStreamEvent
344
+ } from "./services/api";
345
+
346
+ interface SourceItem {
347
+ title: string;
348
+ url: string;
349
+ snippet: string;
350
+ raw: string;
351
+ }
352
+
353
+ interface ToolCallLog {
354
+ eventId: number;
355
+ agent: string;
356
+ tool: string;
357
+ parameters: Record<string, unknown>;
358
+ result: string;
359
+ noteId: string | null;
360
+ notePath: string | null;
361
+ timestamp: number;
362
+ }
363
+
364
+ interface TodoTaskView {
365
+ id: number;
366
+ title: string;
367
+ intent: string;
368
+ query: string;
369
+ status: string;
370
+ summary: string;
371
+ sourcesSummary: string;
372
+ sourceItems: SourceItem[];
373
+ notices: string[];
374
+ noteId: string | null;
375
+ notePath: string | null;
376
+ toolCalls: ToolCallLog[];
377
+ }
378
+
379
+ const form = reactive({
380
+ topic: "",
381
+ searchApi: ""
382
+ });
383
+
384
+ const loading = ref(false);
385
+ const error = ref("");
386
+ const progressLogs = ref<string[]>([]);
387
+ const logsCollapsed = ref(false);
388
+ const isExpanded = ref(false);
389
+
390
+ const todoTasks = ref<TodoTaskView[]>([]);
391
+ const activeTaskId = ref<number | null>(null);
392
+ const reportMarkdown = ref("");
393
+
394
+ const summaryHighlight = ref(false);
395
+ const sourcesHighlight = ref(false);
396
+ const reportHighlight = ref(false);
397
+ const toolHighlight = ref(false);
398
+
399
+ let currentController: AbortController | null = null;
400
+
401
+ const searchOptions = [
402
+ "advanced",
403
+ "duckduckgo",
404
+ "tavily",
405
+ "perplexity",
406
+ "searxng"
407
+ ];
408
+
409
+ const TASK_STATUS_LABEL: Record<string, string> = {
410
+ pending: "Pending",
411
+ in_progress: "In Progress",
412
+ completed: "Completed",
413
+ skipped: "Skipped"
414
+ };
415
+
416
+ function formatTaskStatus(status: string): string {
417
+ return TASK_STATUS_LABEL[status] ?? status;
418
+ }
419
+
420
+ const totalTasks = computed(() => todoTasks.value.length);
421
+ const completedTasks = computed(() =>
422
+ todoTasks.value.filter((task) => task.status === "completed").length
423
+ );
424
+
425
+ const currentTask = computed(() => {
426
+ if (activeTaskId.value !== null) {
427
+ return todoTasks.value.find((task) => task.id === activeTaskId.value) ?? null;
428
+ }
429
+ return todoTasks.value[0] ?? null;
430
+ });
431
+
432
+ const currentTaskSources = computed(() => currentTask.value?.sourceItems ?? []);
433
+ const currentTaskSummary = computed(() => currentTask.value?.summary ?? "");
434
+ const currentTaskTitle = computed(() => currentTask.value?.title ?? "");
435
+ const currentTaskIntent = computed(() => currentTask.value?.intent ?? "");
436
+ const currentTaskQuery = computed(() => currentTask.value?.query ?? "");
437
+ const currentTaskNoteId = computed(() => currentTask.value?.noteId ?? "");
438
+ const currentTaskNotePath = computed(() => currentTask.value?.notePath ?? "");
439
+ const currentTaskToolCalls = computed(
440
+ () => currentTask.value?.toolCalls ?? []
441
+ );
442
+
443
+ const pulse = (flag: typeof summaryHighlight) => {
444
+ flag.value = false;
445
+ requestAnimationFrame(() => {
446
+ flag.value = true;
447
+ window.setTimeout(() => {
448
+ flag.value = false;
449
+ }, 1200);
450
+ });
451
+ };
452
+
453
+ function parseSources(raw: string): SourceItem[] {
454
+ if (!raw) {
455
+ return [];
456
+ }
457
+
458
+ const items: SourceItem[] = [];
459
+ const lines = raw.split("\n");
460
+
461
+ let current: SourceItem | null = null;
462
+ const truncate = (value: string, max = 360) => {
463
+ const trimmed = value.trim();
464
+ return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
465
+ };
466
+
467
+ const flush = () => {
468
+ if (!current) {
469
+ return;
470
+ }
471
+ const normalized: SourceItem = {
472
+ title: current.title?.trim() || "",
473
+ url: current.url?.trim() || "",
474
+ snippet: current.snippet ? truncate(current.snippet) : "",
475
+ raw: current.raw ? truncate(current.raw, 420) : ""
476
+ };
477
+
478
+ if (
479
+ normalized.title ||
480
+ normalized.url ||
481
+ normalized.snippet ||
482
+ normalized.raw
483
+ ) {
484
+ if (!normalized.title && normalized.url) {
485
+ normalized.title = normalized.url;
486
+ }
487
+ items.push(normalized);
488
+ }
489
+ current = null;
490
+ };
491
+
492
+ const ensureCurrent = () => {
493
+ if (!current) {
494
+ current = { title: "", url: "", snippet: "", raw: "" };
495
+ }
496
+ };
497
+
498
+ for (const line of lines) {
499
+ const trimmed = line.trim();
500
+ if (!trimmed) {
501
+ continue;
502
+ }
503
+
504
+ if (/^\*/.test(trimmed) && trimmed.includes(" : ")) {
505
+ flush();
506
+ const withoutBullet = trimmed.replace(/^\*\s*/, "");
507
+ const [titlePart, urlPart] = withoutBullet.split(" : ");
508
+ current = {
509
+ title: titlePart?.trim() || "",
510
+ url: urlPart?.trim() || "",
511
+ snippet: "",
512
+ raw: ""
513
+ };
514
+ continue;
515
+ }
516
+
517
+ if (/^(Source|Info Source)\s*:/.test(trimmed)) {
518
+ flush();
519
+ const [, titlePart = ""] = trimmed.split(/:\s*(.+)/);
520
+ current = {
521
+ title: titlePart.trim(),
522
+ url: "",
523
+ snippet: "",
524
+ raw: ""
525
+ };
526
+ continue;
527
+ }
528
+
529
+ if (/^URL\s*:/.test(trimmed)) {
530
+ ensureCurrent();
531
+ const [, urlPart = ""] = trimmed.split(/:\s*(.+)/);
532
+ current!.url = urlPart.trim();
533
+ continue;
534
+ }
535
+
536
+ if (
537
+ /^(Most relevant content from source|Info Content)\s*:/.test(trimmed)
538
+ ) {
539
+ ensureCurrent();
540
+ const [, contentPart = ""] = trimmed.split(/:\s*(.+)/);
541
+ current!.snippet = contentPart.trim();
542
+ continue;
543
+ }
544
+
545
+ if (
546
+ /^(Full source content limited to|Info content limited to)\s*:/.test(trimmed)
547
+ ) {
548
+ ensureCurrent();
549
+ const [, rawPart = ""] = trimmed.split(/:\s*(.+)/);
550
+ current!.raw = rawPart.trim();
551
+ continue;
552
+ }
553
+
554
+ if (/^https?:\/\//.test(trimmed)) {
555
+ ensureCurrent();
556
+ if (!current!.url) {
557
+ current!.url = trimmed;
558
+ continue;
559
+ }
560
+ }
561
+
562
+ ensureCurrent();
563
+ current!.raw = current!.raw ? `${current!.raw}\n${trimmed}` : trimmed;
564
+ }
565
+
566
+ flush();
567
+ return items;
568
+ }
569
+
570
+ function extractOptionalString(value: unknown): string | null {
571
+ if (typeof value !== "string") {
572
+ return null;
573
+ }
574
+ const trimmed = value.trim();
575
+ return trimmed ? trimmed : null;
576
+ }
577
+
578
+ function ensureRecord(value: unknown): Record<string, unknown> {
579
+ if (value && typeof value === "object" && !Array.isArray(value)) {
580
+ return value as Record<string, unknown>;
581
+ }
582
+ return {};
583
+ }
584
+
585
+ function applyNoteMetadata(
586
+ task: TodoTaskView,
587
+ payload: Record<string, unknown>
588
+ ): void {
589
+ const noteId = extractOptionalString(payload.note_id);
590
+ if (noteId) {
591
+ task.noteId = noteId;
592
+ }
593
+ const notePath = extractOptionalString(payload.note_path);
594
+ if (notePath) {
595
+ task.notePath = notePath;
596
+ }
597
+ }
598
+
599
+ function formatToolParameters(parameters: Record<string, unknown>): string {
600
+ try {
601
+ return JSON.stringify(parameters, null, 2);
602
+ } catch (error) {
603
+ console.warn("Unable to format tool parameters", error, parameters);
604
+ return Object.entries(parameters)
605
+ .map(([key, value]) => `${key}: ${String(value)}`)
606
+ .join("\n");
607
+ }
608
+ }
609
+
610
+ function formatToolResult(result: string): string {
611
+ const trimmed = result.trim();
612
+ const limit = 900;
613
+ if (trimmed.length > limit) {
614
+ return `${trimmed.slice(0, limit)}…`;
615
+ }
616
+ return trimmed;
617
+ }
618
+
619
+ async function copyNotePath(path: string | null | undefined) {
620
+ if (!path) {
621
+ return;
622
+ }
623
+
624
+ try {
625
+ await navigator.clipboard.writeText(path);
626
+ progressLogs.value.push(`Note path copied: ${path}`);
627
+ } catch (error) {
628
+ console.warn("Unable to copy to clipboard directly", error);
629
+ window.prompt("Copy the following note path", path);
630
+ progressLogs.value.push("Please copy the note path manually");
631
+ }
632
+ }
633
+
634
+ function resetWorkflowState() {
635
+ todoTasks.value = [];
636
+ activeTaskId.value = null;
637
+ reportMarkdown.value = "";
638
+ progressLogs.value = [];
639
+ summaryHighlight.value = false;
640
+ sourcesHighlight.value = false;
641
+ reportHighlight.value = false;
642
+ toolHighlight.value = false;
643
+ logsCollapsed.value = false;
644
+ }
645
+
646
+ function findTask(taskId: unknown): TodoTaskView | undefined {
647
+ const numeric =
648
+ typeof taskId === "number"
649
+ ? taskId
650
+ : typeof taskId === "string"
651
+ ? Number(taskId)
652
+ : NaN;
653
+ if (Number.isNaN(numeric)) {
654
+ return undefined;
655
+ }
656
+ return todoTasks.value.find((task) => task.id === numeric);
657
+ }
658
+
659
+ function upsertTaskMetadata(task: TodoTaskView, payload: Record<string, unknown>) {
660
+ if (typeof payload.title === "string" && payload.title.trim()) {
661
+ task.title = payload.title.trim();
662
+ }
663
+ if (typeof payload.intent === "string" && payload.intent.trim()) {
664
+ task.intent = payload.intent.trim();
665
+ }
666
+ if (typeof payload.query === "string" && payload.query.trim()) {
667
+ task.query = payload.query.trim();
668
+ }
669
+ }
670
+
671
+ const handleSubmit = async () => {
672
+ if (!form.topic.trim()) {
673
+ error.value = "Please enter a research topic";
674
+ return;
675
+ }
676
+
677
+ if (currentController) {
678
+ currentController.abort();
679
+ currentController = null;
680
+ }
681
+
682
+ loading.value = true;
683
+ error.value = "";
684
+ isExpanded.value = true;
685
+ resetWorkflowState();
686
+
687
+ const controller = new AbortController();
688
+ currentController = controller;
689
+
690
+ const payload = {
691
+ topic: form.topic.trim(),
692
+ search_api: form.searchApi || undefined
693
+ };
694
+
695
+ try {
696
+ await runResearchStream(
697
+ payload,
698
+ (event: ResearchStreamEvent) => {
699
+ if (event.type === "status") {
700
+ const message =
701
+ typeof event.message === "string" && event.message.trim()
702
+ ? event.message
703
+ : "Workflow status update";
704
+ progressLogs.value.push(message);
705
+
706
+ const payload = event as Record<string, unknown>;
707
+ const task = findTask(payload.task_id);
708
+ if (task && message) {
709
+ task.notices.push(message);
710
+ applyNoteMetadata(task, payload);
711
+ }
712
+ return;
713
+ }
714
+
715
+ if (event.type === "todo_list") {
716
+ const tasks = Array.isArray(event.tasks)
717
+ ? (event.tasks as Record<string, unknown>[])
718
+ : [];
719
+
720
+ todoTasks.value = tasks.map((item, index) => {
721
+ const rawId =
722
+ typeof item.id === "number"
723
+ ? item.id
724
+ : typeof item.id === "string"
725
+ ? Number(item.id)
726
+ : index + 1;
727
+ const id = Number.isFinite(rawId) ? Number(rawId) : index + 1;
728
+ const noteId =
729
+ typeof item.note_id === "string" && item.note_id.trim()
730
+ ? item.note_id.trim()
731
+ : null;
732
+ const notePath =
733
+ typeof item.note_path === "string" && item.note_path.trim()
734
+ ? item.note_path.trim()
735
+ : null;
736
+
737
+ return {
738
+ id,
739
+ title:
740
+ typeof item.title === "string" && item.title.trim()
741
+ ? item.title.trim()
742
+ : `Task ${id}`,
743
+ intent:
744
+ typeof item.intent === "string" && item.intent.trim()
745
+ ? item.intent.trim()
746
+ : "Explore key information related to the topic",
747
+ query:
748
+ typeof item.query === "string" && item.query.trim()
749
+ ? item.query.trim()
750
+ : form.topic.trim(),
751
+ status:
752
+ typeof item.status === "string" && item.status.trim()
753
+ ? item.status.trim()
754
+ : "pending",
755
+ summary: "",
756
+ sourcesSummary: "",
757
+ sourceItems: [],
758
+ notices: [],
759
+ noteId,
760
+ notePath,
761
+ toolCalls: []
762
+ } as TodoTaskView;
763
+ });
764
+
765
+ if (todoTasks.value.length) {
766
+ activeTaskId.value = todoTasks.value[0].id;
767
+ progressLogs.value.push("Task list generated");
768
+ } else {
769
+ progressLogs.value.push("No task list generated, continuing with default task");
770
+ }
771
+ return;
772
+ }
773
+
774
+ if (event.type === "task_status") {
775
+ const payload = event as Record<string, unknown>;
776
+ const task = findTask(event.task_id);
777
+ if (!task) {
778
+ return;
779
+ }
780
+
781
+ upsertTaskMetadata(task, payload);
782
+ applyNoteMetadata(task, payload);
783
+ const status =
784
+ typeof event.status === "string" && event.status.trim()
785
+ ? event.status.trim()
786
+ : task.status;
787
+ task.status = status;
788
+
789
+ if (status === "in_progress") {
790
+ task.summary = "";
791
+ task.sourcesSummary = "";
792
+ task.sourceItems = [];
793
+ task.notices = [];
794
+ activeTaskId.value = task.id;
795
+ progressLogs.value.push(`Starting task: ${task.title}`);
796
+ } else if (status === "completed") {
797
+ if (typeof event.summary === "string" && event.summary.trim()) {
798
+ task.summary = event.summary.trim();
799
+ }
800
+ if (
801
+ typeof event.sources_summary === "string" &&
802
+ event.sources_summary.trim()
803
+ ) {
804
+ task.sourcesSummary = event.sources_summary.trim();
805
+ task.sourceItems = parseSources(task.sourcesSummary);
806
+ }
807
+ progressLogs.value.push(`Completed task: ${task.title}`);
808
+ if (activeTaskId.value === task.id) {
809
+ pulse(summaryHighlight);
810
+ pulse(sourcesHighlight);
811
+ }
812
+ } else if (status === "skipped") {
813
+ progressLogs.value.push(`Task skipped: ${task.title}`);
814
+ }
815
+ return;
816
+ }
817
+
818
+ if (event.type === "sources") {
819
+ const payload = event as Record<string, unknown>;
820
+ const task = findTask(event.task_id);
821
+ if (!task) {
822
+ return;
823
+ }
824
+
825
+ const textCandidates = [
826
+ payload.latest_sources,
827
+ payload.sources_summary,
828
+ payload.raw_context
829
+ ];
830
+ const latestText = textCandidates
831
+ .map((value) => (typeof value === "string" ? value.trim() : ""))
832
+ .find((value) => value);
833
+
834
+ if (latestText) {
835
+ task.sourcesSummary = latestText;
836
+ task.sourceItems = parseSources(latestText);
837
+ if (activeTaskId.value === task.id) {
838
+ pulse(sourcesHighlight);
839
+ }
840
+ progressLogs.value.push(`Updated task sources: ${task.title}`);
841
+ }
842
+
843
+ if (typeof payload.backend === "string") {
844
+ progressLogs.value.push(
845
+ `Current search backend: ${payload.backend}`
846
+ );
847
+ }
848
+
849
+ applyNoteMetadata(task, payload);
850
+
851
+ return;
852
+ }
853
+
854
+ if (event.type === "task_summary_chunk") {
855
+ const payload = event as Record<string, unknown>;
856
+ const task = findTask(event.task_id);
857
+ if (!task) {
858
+ return;
859
+ }
860
+ const chunk =
861
+ typeof event.content === "string" ? event.content : "";
862
+ task.summary += chunk;
863
+ applyNoteMetadata(task, payload);
864
+ if (activeTaskId.value === task.id) {
865
+ pulse(summaryHighlight);
866
+ }
867
+ return;
868
+ }
869
+
870
+ if (event.type === "tool_call") {
871
+ const payload = event as Record<string, unknown>;
872
+ const eventId =
873
+ typeof payload.event_id === "number"
874
+ ? payload.event_id
875
+ : Date.now();
876
+ const agent =
877
+ typeof payload.agent === "string" && payload.agent.trim()
878
+ ? payload.agent.trim()
879
+ : "Agent";
880
+ const tool =
881
+ typeof payload.tool === "string" && payload.tool.trim()
882
+ ? payload.tool.trim()
883
+ : "tool";
884
+ const parameters = ensureRecord(payload.parameters);
885
+ const result =
886
+ typeof payload.result === "string" ? payload.result : "";
887
+ const noteId = extractOptionalString(payload.note_id);
888
+ const notePath = extractOptionalString(payload.note_path);
889
+
890
+ const task = findTask(payload.task_id);
891
+ if (task) {
892
+ task.toolCalls.push({
893
+ eventId,
894
+ agent,
895
+ tool,
896
+ parameters,
897
+ result,
898
+ noteId,
899
+ notePath,
900
+ timestamp: Date.now()
901
+ });
902
+ if (noteId) {
903
+ task.noteId = noteId;
904
+ }
905
+ if (notePath) {
906
+ task.notePath = notePath;
907
+ }
908
+ const logSummary = noteId
909
+ ? `${agent} called ${tool} (Task ${task.id}, Note ${noteId})`
910
+ : `${agent} called ${tool} (Task ${task.id})`;
911
+ progressLogs.value.push(logSummary);
912
+ if (activeTaskId.value === task.id) {
913
+ pulse(toolHighlight);
914
+ }
915
+ } else {
916
+ progressLogs.value.push(`${agent} called ${tool}`);
917
+ }
918
+ return;
919
+ }
920
+
921
+ if (event.type === "final_report") {
922
+ const report =
923
+ typeof event.report === "string" && event.report.trim()
924
+ ? event.report.trim()
925
+ : "";
926
+ reportMarkdown.value = report || "Report generation failed, no valid content obtained";
927
+ pulse(reportHighlight);
928
+ progressLogs.value.push("Final report generated");
929
+ return;
930
+ }
931
+
932
+ if (event.type === "error") {
933
+ const detail =
934
+ typeof event.detail === "string" && event.detail.trim()
935
+ ? event.detail
936
+ : "An error occurred during research";
937
+ error.value = detail;
938
+ progressLogs.value.push("Research failed, workflow stopped");
939
+ }
940
+ },
941
+ { signal: controller.signal }
942
+ );
943
+
944
+ if (!reportMarkdown.value) {
945
+ reportMarkdown.value = "No report generated";
946
+ }
947
+ } catch (err) {
948
+ if (err instanceof DOMException && err.name === "AbortError") {
949
+ progressLogs.value.push("Current research task cancelled");
950
+ } else {
951
+ error.value = err instanceof Error ? err.message : "Request failed";
952
+ }
953
+ } finally {
954
+ loading.value = false;
955
+ if (currentController === controller) {
956
+ currentController = null;
957
+ }
958
+ }
959
+ };
960
+
961
+ const cancelResearch = () => {
962
+ if (!loading.value || !currentController) {
963
+ return;
964
+ }
965
+ progressLogs.value.push("Attempting to cancel current research task...");
966
+ currentController.abort();
967
+ };
968
+
969
+ const goBack = () => {
970
+ if (loading.value) {
971
+ return; // Cannot go back while research is in progress
972
+ }
973
+ isExpanded.value = false;
974
+ };
975
+
976
+ const startNewResearch = () => {
977
+ if (loading.value) {
978
+ cancelResearch();
979
+ }
980
+ resetWorkflowState();
981
+ isExpanded.value = false;
982
+ form.topic = "";
983
+ form.searchApi = "";
984
+ };
985
+
986
+ onBeforeUnmount(() => {
987
+ if (currentController) {
988
+ currentController.abort();
989
+ currentController = null;
990
+ }
991
+ });
992
+ </script>
993
+
994
+
995
+ <style scoped>
996
+ .app-shell {
997
+ position: relative;
998
+ min-height: 100vh;
999
+ padding: 72px 24px;
1000
+ display: flex;
1001
+ justify-content: center;
1002
+ align-items: center;
1003
+ background: radial-gradient(circle at 20% 20%, #f8fafc, #dbeafe 60%);
1004
+ color: #1f2937;
1005
+ overflow: hidden;
1006
+ box-sizing: border-box;
1007
+ transition: padding 0.4s ease;
1008
+ }
1009
+
1010
+ .app-shell.expanded {
1011
+ padding: 0;
1012
+ align-items: stretch;
1013
+ }
1014
+
1015
+ .aurora {
1016
+ position: absolute;
1017
+ inset: 0;
1018
+ pointer-events: none;
1019
+ opacity: 0.55;
1020
+ }
1021
+
1022
+ .aurora span {
1023
+ position: absolute;
1024
+ width: 45vw;
1025
+ height: 45vw;
1026
+ max-width: 520px;
1027
+ max-height: 520px;
1028
+ background: radial-gradient(circle, rgba(148, 197, 255, 0.35), transparent 60%);
1029
+ filter: blur(90px);
1030
+ animation: float 26s infinite linear;
1031
+ }
1032
+
1033
+ .aurora span:nth-child(1) {
1034
+ top: -20%;
1035
+ left: -18%;
1036
+ animation-delay: 0s;
1037
+ }
1038
+
1039
+ .aurora span:nth-child(2) {
1040
+ bottom: -25%;
1041
+ right: -20%;
1042
+ background: radial-gradient(circle, rgba(166, 139, 255, 0.28), transparent 60%);
1043
+ animation-delay: -9s;
1044
+ }
1045
+
1046
+ .aurora span:nth-child(3) {
1047
+ top: 35%;
1048
+ left: 45%;
1049
+ background: radial-gradient(circle, rgba(164, 219, 216, 0.26), transparent 60%);
1050
+ animation-delay: -16s;
1051
+ }
1052
+
1053
+ .layout {
1054
+ position: relative;
1055
+ width: 100%;
1056
+ display: flex;
1057
+ gap: 24px;
1058
+ z-index: 1;
1059
+ transition: all 0.4s ease;
1060
+ }
1061
+
1062
+ .layout-centered {
1063
+ max-width: 600px;
1064
+ justify-content: center;
1065
+ align-items: center;
1066
+ }
1067
+
1068
+ .layout-fullscreen {
1069
+ height: 100vh;
1070
+ max-width: 100%;
1071
+ gap: 0;
1072
+ align-items: stretch;
1073
+ }
1074
+
1075
+ .panel {
1076
+ position: relative;
1077
+ flex: 1 1 360px;
1078
+ padding: 24px;
1079
+ border-radius: 20px;
1080
+ background: rgba(255, 255, 255, 0.95);
1081
+ border: 1px solid rgba(148, 163, 184, 0.18);
1082
+ box-shadow: 0 24px 48px rgba(15, 23, 42, 0.12);
1083
+ backdrop-filter: blur(8px);
1084
+ overflow: hidden;
1085
+ }
1086
+
1087
+ .panel-form {
1088
+ max-width: 420px;
1089
+ }
1090
+
1091
+ .panel-centered {
1092
+ width: 100%;
1093
+ max-width: 600px;
1094
+ padding: 40px;
1095
+ box-shadow: 0 32px 64px rgba(15, 23, 42, 0.15);
1096
+ transform: scale(1);
1097
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
1098
+ }
1099
+
1100
+ .panel-centered:hover {
1101
+ transform: scale(1.02);
1102
+ box-shadow: 0 40px 80px rgba(15, 23, 42, 0.2);
1103
+ }
1104
+
1105
+ .panel-result {
1106
+ min-width: 360px;
1107
+ flex: 2 1 420px;
1108
+ }
1109
+
1110
+ .panel::before {
1111
+ content: "";
1112
+ position: absolute;
1113
+ inset: 0;
1114
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.12), rgba(125, 86, 255, 0.1));
1115
+ opacity: 0;
1116
+ transition: opacity 0.35s ease;
1117
+ z-index: 0;
1118
+ }
1119
+
1120
+ .panel:hover::before {
1121
+ opacity: 1;
1122
+ }
1123
+
1124
+ .panel > * {
1125
+ position: relative;
1126
+ z-index: 1;
1127
+ }
1128
+
1129
+ .panel-form h1 {
1130
+ margin: 0;
1131
+ font-size: 26px;
1132
+ letter-spacing: 0.01em;
1133
+ }
1134
+
1135
+ .panel-form p {
1136
+ margin: 4px 0 0;
1137
+ color: #64748b;
1138
+ font-size: 13px;
1139
+ }
1140
+
1141
+ .panel-head {
1142
+ display: flex;
1143
+ align-items: center;
1144
+ gap: 16px;
1145
+ margin-bottom: 24px;
1146
+ }
1147
+
1148
+ .logo {
1149
+ width: 52px;
1150
+ height: 52px;
1151
+ display: grid;
1152
+ place-items: center;
1153
+ border-radius: 16px;
1154
+ background: linear-gradient(135deg, #2563eb, #7c3aed);
1155
+ box-shadow: 0 12px 28px rgba(59, 130, 246, 0.4);
1156
+ }
1157
+
1158
+ .logo svg {
1159
+ width: 28px;
1160
+ height: 28px;
1161
+ fill: #f8fafc;
1162
+ }
1163
+
1164
+ .form {
1165
+ display: flex;
1166
+ flex-direction: column;
1167
+ gap: 18px;
1168
+ }
1169
+
1170
+ .field {
1171
+ display: flex;
1172
+ flex-direction: column;
1173
+ gap: 10px;
1174
+ }
1175
+
1176
+ .field span {
1177
+ font-weight: 600;
1178
+ color: #475569;
1179
+ }
1180
+
1181
+ textarea,
1182
+ input,
1183
+ select {
1184
+ padding: 14px 16px;
1185
+ border-radius: 16px;
1186
+ border: 1px solid rgba(148, 163, 184, 0.35);
1187
+ background: rgba(255, 255, 255, 0.92);
1188
+ color: #1f2937;
1189
+ font-size: 14px;
1190
+ transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
1191
+ }
1192
+
1193
+ textarea:focus,
1194
+ input:focus,
1195
+ select:focus {
1196
+ outline: none;
1197
+ border-color: rgba(37, 99, 235, 0.65);
1198
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
1199
+ background: #ffffff;
1200
+ }
1201
+
1202
+ .options {
1203
+ display: flex;
1204
+ gap: 16px;
1205
+ flex-wrap: wrap;
1206
+ }
1207
+
1208
+ .option {
1209
+ flex: 1;
1210
+ min-width: 140px;
1211
+ }
1212
+
1213
+ .form-actions {
1214
+ display: flex;
1215
+ align-items: center;
1216
+ gap: 12px;
1217
+ flex-wrap: wrap;
1218
+ }
1219
+
1220
+ .submit {
1221
+ align-self: flex-start;
1222
+ padding: 12px 24px;
1223
+ border-radius: 16px;
1224
+ border: none;
1225
+ background: linear-gradient(135deg, #2563eb, #7c3aed);
1226
+ color: #ffffff;
1227
+ font-size: 15px;
1228
+ font-weight: 600;
1229
+ cursor: pointer;
1230
+ transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;
1231
+ display: inline-flex;
1232
+ align-items: center;
1233
+ gap: 10px;
1234
+ position: relative;
1235
+ }
1236
+
1237
+ .submit-label {
1238
+ display: inline-flex;
1239
+ align-items: center;
1240
+ gap: 10px;
1241
+ }
1242
+
1243
+ .submit .spinner {
1244
+ width: 18px;
1245
+ height: 18px;
1246
+ fill: none;
1247
+ stroke: rgba(255, 255, 255, 0.85);
1248
+ stroke-linecap: round;
1249
+ animation: spin 1s linear infinite;
1250
+ }
1251
+
1252
+ .submit:disabled {
1253
+ opacity: 0.7;
1254
+ cursor: not-allowed;
1255
+ }
1256
+
1257
+ .submit:not(:disabled):hover {
1258
+ transform: translateY(-2px);
1259
+ box-shadow: 0 12px 28px rgba(37, 99, 235, 0.28);
1260
+ }
1261
+
1262
+ .secondary-btn {
1263
+ padding: 10px 18px;
1264
+ border-radius: 14px;
1265
+ background: rgba(148, 163, 184, 0.12);
1266
+ border: 1px solid rgba(148, 163, 184, 0.28);
1267
+ color: #1f2937;
1268
+ font-size: 14px;
1269
+ font-weight: 500;
1270
+ cursor: pointer;
1271
+ transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
1272
+ }
1273
+
1274
+ .secondary-btn:hover {
1275
+ background: rgba(148, 163, 184, 0.2);
1276
+ border-color: rgba(148, 163, 184, 0.35);
1277
+ color: #0f172a;
1278
+ }
1279
+
1280
+ .error-chip {
1281
+ margin-top: 16px;
1282
+ display: inline-flex;
1283
+ align-items: center;
1284
+ gap: 8px;
1285
+ padding: 10px 14px;
1286
+ background: rgba(248, 113, 113, 0.12);
1287
+ border: 1px solid rgba(248, 113, 113, 0.35);
1288
+ border-radius: 14px;
1289
+ color: #b91c1c;
1290
+ font-size: 14px;
1291
+ }
1292
+
1293
+ .error-chip svg {
1294
+ width: 18px;
1295
+ height: 18px;
1296
+ fill: currentColor;
1297
+ }
1298
+
1299
+ .panel-result {
1300
+ display: flex;
1301
+ flex-direction: column;
1302
+ gap: 18px;
1303
+ }
1304
+
1305
+ .status-bar {
1306
+ display: flex;
1307
+ align-items: center;
1308
+ justify-content: space-between;
1309
+ gap: 12px;
1310
+ flex-wrap: wrap;
1311
+ }
1312
+
1313
+ .status-main {
1314
+ display: flex;
1315
+ align-items: center;
1316
+ gap: 12px;
1317
+ flex-wrap: wrap;
1318
+ }
1319
+
1320
+ .status-controls {
1321
+ display: flex;
1322
+ gap: 8px;
1323
+ }
1324
+
1325
+ .status-chip {
1326
+ display: inline-flex;
1327
+ align-items: center;
1328
+ gap: 8px;
1329
+ background: rgba(191, 219, 254, 0.28);
1330
+ padding: 8px 14px;
1331
+ border-radius: 999px;
1332
+ font-size: 13px;
1333
+ color: #1f2937;
1334
+ border: 1px solid rgba(59, 130, 246, 0.35);
1335
+ transition: background 0.3s ease, color 0.3s ease;
1336
+ }
1337
+
1338
+ .status-chip.active {
1339
+ background: rgba(129, 140, 248, 0.2);
1340
+ border-color: rgba(129, 140, 248, 0.4);
1341
+ color: #1e293b;
1342
+ }
1343
+
1344
+ .status-chip .dot {
1345
+ width: 8px;
1346
+ height: 8px;
1347
+ border-radius: 999px;
1348
+ background: #2563eb;
1349
+ box-shadow: 0 0 12px rgba(37, 99, 235, 0.45);
1350
+ animation: pulse 1.8s ease-in-out infinite;
1351
+ }
1352
+
1353
+ .status-meta {
1354
+ color: #64748b;
1355
+ font-size: 13px;
1356
+ }
1357
+
1358
+ .timeline-wrapper {
1359
+ margin-top: 12px;
1360
+ max-height: 220px;
1361
+ overflow-y: auto;
1362
+ padding-right: 8px;
1363
+ scrollbar-width: thin;
1364
+ scrollbar-color: rgba(129, 140, 248, 0.45) rgba(226, 232, 240, 0.6);
1365
+ }
1366
+
1367
+ .timeline-wrapper::-webkit-scrollbar {
1368
+ width: 6px;
1369
+ }
1370
+
1371
+ .timeline-wrapper::-webkit-scrollbar-track {
1372
+ background: rgba(226, 232, 240, 0.6);
1373
+ border-radius: 999px;
1374
+ }
1375
+
1376
+ .timeline-wrapper::-webkit-scrollbar-thumb {
1377
+ background: linear-gradient(180deg, rgba(129, 140, 248, 0.8), rgba(59, 130, 246, 0.7));
1378
+ border-radius: 999px;
1379
+ }
1380
+
1381
+ .timeline-wrapper::-webkit-scrollbar-thumb:hover {
1382
+ background: linear-gradient(180deg, rgba(99, 102, 241, 0.9), rgba(37, 99, 235, 0.8));
1383
+ }
1384
+
1385
+ .timeline {
1386
+ list-style: none;
1387
+ padding: 0;
1388
+ margin: 0;
1389
+ display: flex;
1390
+ flex-direction: column;
1391
+ gap: 14px;
1392
+ position: relative;
1393
+ padding-left: 12px;
1394
+ }
1395
+
1396
+ .timeline::before {
1397
+ content: "";
1398
+ position: absolute;
1399
+ top: 8px;
1400
+ bottom: 8px;
1401
+ left: 0;
1402
+ width: 2px;
1403
+ background: linear-gradient(180deg, rgba(59, 130, 246, 0.35), rgba(129, 140, 248, 0.15));
1404
+ }
1405
+
1406
+ .timeline li {
1407
+ position: relative;
1408
+ padding-left: 24px;
1409
+ color: #1e293b;
1410
+ font-size: 14px;
1411
+ line-height: 1.5;
1412
+ }
1413
+
1414
+ .timeline-node {
1415
+ position: absolute;
1416
+ left: -12px;
1417
+ top: 6px;
1418
+ width: 10px;
1419
+ height: 10px;
1420
+ border-radius: 999px;
1421
+ background: linear-gradient(135deg, #38bdf8, #7c3aed);
1422
+ box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.22);
1423
+ }
1424
+
1425
+ .timeline-enter-active,
1426
+ .timeline-leave-active {
1427
+ transition: all 0.35s ease, opacity 0.35s ease;
1428
+ }
1429
+
1430
+ .timeline-enter-from,
1431
+ .timeline-leave-to {
1432
+ opacity: 0;
1433
+ transform: translateY(-6px);
1434
+ }
1435
+
1436
+ .tasks-section {
1437
+ display: grid;
1438
+ grid-template-columns: 280px 1fr;
1439
+ gap: 20px;
1440
+ align-items: start;
1441
+ }
1442
+
1443
+ @media (max-width: 960px) {
1444
+ .tasks-section {
1445
+ grid-template-columns: 1fr;
1446
+ }
1447
+ }
1448
+
1449
+ .tasks-list {
1450
+ background: rgba(255, 255, 255, 0.92);
1451
+ border: 1px solid rgba(148, 163, 184, 0.26);
1452
+ border-radius: 18px;
1453
+ padding: 18px;
1454
+ display: flex;
1455
+ flex-direction: column;
1456
+ gap: 16px;
1457
+ box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
1458
+ }
1459
+
1460
+ .tasks-list h3 {
1461
+ margin: 0;
1462
+ font-size: 16px;
1463
+ font-weight: 600;
1464
+ color: #1f2937;
1465
+ }
1466
+
1467
+ .tasks-list ul {
1468
+ list-style: none;
1469
+ margin: 0;
1470
+ padding: 0;
1471
+ display: flex;
1472
+ flex-direction: column;
1473
+ gap: 12px;
1474
+ }
1475
+
1476
+ .task-item {
1477
+ border-radius: 14px;
1478
+ border: 1px solid transparent;
1479
+ transition: border-color 0.2s ease, background 0.2s ease;
1480
+ }
1481
+
1482
+ .task-item.completed {
1483
+ border-color: rgba(56, 189, 248, 0.35);
1484
+ background: rgba(191, 219, 254, 0.28);
1485
+ }
1486
+
1487
+ .task-item.active {
1488
+ border-color: rgba(129, 140, 248, 0.5);
1489
+ background: rgba(224, 231, 255, 0.5);
1490
+ }
1491
+
1492
+ .task-button {
1493
+ width: 100%;
1494
+ display: flex;
1495
+ align-items: center;
1496
+ justify-content: space-between;
1497
+ gap: 12px;
1498
+ padding: 12px 14px 6px;
1499
+ background: transparent;
1500
+ border: none;
1501
+ color: inherit;
1502
+ cursor: pointer;
1503
+ text-align: left;
1504
+ }
1505
+
1506
+ .task-title {
1507
+ font-weight: 600;
1508
+ font-size: 14px;
1509
+ color: #1e293b;
1510
+ }
1511
+
1512
+ .task-status {
1513
+ display: inline-flex;
1514
+ align-items: center;
1515
+ justify-content: center;
1516
+ padding: 4px 10px;
1517
+ border-radius: 999px;
1518
+ font-size: 12px;
1519
+ font-weight: 500;
1520
+ color: #1f2937;
1521
+ background: rgba(148, 163, 184, 0.2);
1522
+ }
1523
+
1524
+ .task-status.pending {
1525
+ background: rgba(148, 163, 184, 0.18);
1526
+ color: #475569;
1527
+ }
1528
+
1529
+ .task-status.in_progress {
1530
+ background: rgba(129, 140, 248, 0.24);
1531
+ color: #312e81;
1532
+ }
1533
+
1534
+ .task-status.completed {
1535
+ background: rgba(34, 197, 94, 0.2);
1536
+ color: #15803d;
1537
+ }
1538
+
1539
+ .task-status.skipped {
1540
+ background: rgba(248, 113, 113, 0.18);
1541
+ color: #b91c1c;
1542
+ }
1543
+
1544
+ .task-intent {
1545
+ margin: 0;
1546
+ padding: 0 14px 12px 14px;
1547
+ font-size: 13px;
1548
+ color: #64748b;
1549
+ }
1550
+
1551
+ .task-detail {
1552
+ background: rgba(255, 255, 255, 0.94);
1553
+ border: 1px solid rgba(148, 163, 184, 0.26);
1554
+ border-radius: 18px;
1555
+ padding: 22px;
1556
+ display: flex;
1557
+ flex-direction: column;
1558
+ gap: 18px;
1559
+ box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.5);
1560
+ }
1561
+
1562
+ .task-header {
1563
+ display: flex;
1564
+ justify-content: space-between;
1565
+ align-items: flex-start;
1566
+ flex-wrap: wrap;
1567
+ gap: 12px;
1568
+ }
1569
+
1570
+ .task-chip-group {
1571
+ display: flex;
1572
+ align-items: center;
1573
+ gap: 8px;
1574
+ flex-wrap: wrap;
1575
+ }
1576
+
1577
+ .task-header h3 {
1578
+ margin: 0;
1579
+ font-size: 18px;
1580
+ font-weight: 600;
1581
+ color: #1f2937;
1582
+ }
1583
+
1584
+ .task-header .muted {
1585
+ margin: 6px 0 0;
1586
+ }
1587
+
1588
+ .task-label {
1589
+ padding: 6px 12px;
1590
+ border-radius: 999px;
1591
+ background: rgba(191, 219, 254, 0.32);
1592
+ border: 1px solid rgba(59, 130, 246, 0.35);
1593
+ font-size: 12px;
1594
+ color: #1e3a8a;
1595
+ }
1596
+
1597
+ .task-label.note-chip {
1598
+ background: rgba(34, 197, 94, 0.2);
1599
+ border-color: rgba(34, 197, 94, 0.35);
1600
+ color: #15803d;
1601
+ }
1602
+
1603
+ .task-label.path-chip {
1604
+ display: inline-flex;
1605
+ align-items: center;
1606
+ gap: 6px;
1607
+ max-width: 360px;
1608
+ background: rgba(56, 189, 248, 0.2);
1609
+ border-color: rgba(56, 189, 248, 0.35);
1610
+ color: #0369a1;
1611
+ overflow: hidden;
1612
+ text-overflow: ellipsis;
1613
+ white-space: nowrap;
1614
+ }
1615
+
1616
+ .path-label {
1617
+ font-weight: 500;
1618
+ }
1619
+
1620
+ .path-text {
1621
+ max-width: 220px;
1622
+ overflow: hidden;
1623
+ text-overflow: ellipsis;
1624
+ white-space: nowrap;
1625
+ }
1626
+
1627
+ .chip-action {
1628
+ border: none;
1629
+ background: rgba(56, 189, 248, 0.2);
1630
+ color: #0369a1;
1631
+ padding: 3px 8px;
1632
+ border-radius: 10px;
1633
+ font-size: 11px;
1634
+ cursor: pointer;
1635
+ transition: background 0.2s ease, color 0.2s ease;
1636
+ }
1637
+
1638
+ .chip-action:hover {
1639
+ background: rgba(14, 165, 233, 0.28);
1640
+ color: #0f172a;
1641
+ }
1642
+
1643
+ .task-notices {
1644
+ background: rgba(191, 219, 254, 0.28);
1645
+ border: 1px solid rgba(96, 165, 250, 0.35);
1646
+ border-radius: 16px;
1647
+ padding: 14px 18px;
1648
+ color: #1f2937;
1649
+ }
1650
+
1651
+ .task-notices h4 {
1652
+ margin: 0 0 8px;
1653
+ font-size: 14px;
1654
+ font-weight: 600;
1655
+ }
1656
+
1657
+ .task-notices ul {
1658
+ list-style: disc;
1659
+ margin: 0 0 0 18px;
1660
+ padding: 0;
1661
+ display: flex;
1662
+ flex-direction: column;
1663
+ gap: 6px;
1664
+ }
1665
+
1666
+ .task-notices li {
1667
+ font-size: 13px;
1668
+ }
1669
+
1670
+ .report-block {
1671
+ background: rgba(255, 255, 255, 0.94);
1672
+ border: 1px solid rgba(148, 163, 184, 0.26);
1673
+ border-radius: 18px;
1674
+ padding: 22px;
1675
+ display: flex;
1676
+ flex-direction: column;
1677
+ gap: 12px;
1678
+ }
1679
+
1680
+ .report-block h3 {
1681
+ margin: 0;
1682
+ font-size: 18px;
1683
+ font-weight: 600;
1684
+ color: #1f2937;
1685
+ }
1686
+
1687
+ .block-pre {
1688
+ font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
1689
+ Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1690
+ font-size: 13px;
1691
+ line-height: 1.7;
1692
+ white-space: pre-wrap;
1693
+ word-break: break-word;
1694
+ color: #1f2937;
1695
+ background: rgba(248, 250, 252, 0.9);
1696
+ padding: 16px;
1697
+ border-radius: 14px;
1698
+ border: 1px solid rgba(148, 163, 184, 0.35);
1699
+ overflow: auto;
1700
+ max-height: 420px;
1701
+ scrollbar-width: thin;
1702
+ scrollbar-color: rgba(129, 140, 248, 0.6) rgba(226, 232, 240, 0.7);
1703
+ }
1704
+
1705
+ .block-pre::-webkit-scrollbar {
1706
+ width: 6px;
1707
+ }
1708
+
1709
+ .block-pre::-webkit-scrollbar-track {
1710
+ background: rgba(226, 232, 240, 0.7);
1711
+ border-radius: 999px;
1712
+ }
1713
+
1714
+ .block-pre::-webkit-scrollbar-thumb {
1715
+ background: linear-gradient(180deg, rgba(99, 102, 241, 0.75), rgba(59, 130, 246, 0.65));
1716
+ border-radius: 999px;
1717
+ }
1718
+
1719
+ .block-pre::-webkit-scrollbar-thumb:hover {
1720
+ background: linear-gradient(180deg, rgba(79, 70, 229, 0.8), rgba(37, 99, 235, 0.75));
1721
+ }
1722
+
1723
+ .summary-block .block-pre,
1724
+ .sources-block .block-pre {
1725
+ max-height: 360px;
1726
+ }
1727
+
1728
+
1729
+ .tools-block {
1730
+ position: relative;
1731
+ margin-top: 16px;
1732
+ padding: 20px;
1733
+ border-radius: 18px;
1734
+ background: rgba(255, 255, 255, 0.94);
1735
+ border: 1px solid rgba(148, 163, 184, 0.18);
1736
+ box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
1737
+ display: flex;
1738
+ flex-direction: column;
1739
+ gap: 12px;
1740
+ }
1741
+
1742
+ .tools-block h3 {
1743
+ margin: 0;
1744
+ font-size: 16px;
1745
+ font-weight: 600;
1746
+ color: #1f2937;
1747
+ letter-spacing: 0.02em;
1748
+ }
1749
+
1750
+ .tool-list {
1751
+ list-style: none;
1752
+ margin: 0;
1753
+ padding: 0;
1754
+ display: flex;
1755
+ flex-direction: column;
1756
+ gap: 12px;
1757
+ }
1758
+
1759
+ .tool-entry {
1760
+ background: rgba(248, 250, 252, 0.95);
1761
+ border: 1px solid rgba(148, 163, 184, 0.24);
1762
+ border-radius: 14px;
1763
+ padding: 14px;
1764
+ display: flex;
1765
+ flex-direction: column;
1766
+ gap: 10px;
1767
+ }
1768
+
1769
+ .tool-entry-header {
1770
+ display: flex;
1771
+ flex-wrap: wrap;
1772
+ gap: 8px;
1773
+ align-items: center;
1774
+ justify-content: space-between;
1775
+ }
1776
+
1777
+ .tool-entry-title {
1778
+ font-weight: 600;
1779
+ color: #1f2937;
1780
+ }
1781
+
1782
+ .tool-entry-note {
1783
+ font-size: 12px;
1784
+ color: #0f766e;
1785
+ }
1786
+
1787
+ .tool-entry-path {
1788
+ margin: 0;
1789
+ font-size: 12px;
1790
+ display: flex;
1791
+ align-items: center;
1792
+ gap: 6px;
1793
+ color: #2563eb;
1794
+ }
1795
+
1796
+ .tool-subtitle {
1797
+ margin: 0;
1798
+ font-size: 13px;
1799
+ color: #475569;
1800
+ font-weight: 500;
1801
+ }
1802
+
1803
+ .tool-pre {
1804
+ font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
1805
+ Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1806
+ font-size: 12px;
1807
+ line-height: 1.6;
1808
+ white-space: pre-wrap;
1809
+ word-break: break-word;
1810
+ color: #1f2937;
1811
+ background: rgba(248, 250, 252, 0.9);
1812
+ padding: 12px;
1813
+ border-radius: 12px;
1814
+ border: 1px solid rgba(148, 163, 184, 0.28);
1815
+ overflow: auto;
1816
+ max-height: 260px;
1817
+ scrollbar-width: thin;
1818
+ scrollbar-color: rgba(129, 140, 248, 0.6) rgba(226, 232, 240, 0.7);
1819
+ }
1820
+
1821
+ .tool-pre::-webkit-scrollbar {
1822
+ width: 6px;
1823
+ }
1824
+
1825
+ .tool-pre::-webkit-scrollbar-track {
1826
+ background: rgba(226, 232, 240, 0.7);
1827
+ }
1828
+
1829
+ .tool-pre::-webkit-scrollbar-thumb {
1830
+ background: rgba(99, 102, 241, 0.7);
1831
+ border-radius: 10px;
1832
+ }
1833
+
1834
+ .link-btn {
1835
+ background: none;
1836
+ border: none;
1837
+ color: #0369a1;
1838
+ cursor: pointer;
1839
+ padding: 0 4px;
1840
+ font-size: 12px;
1841
+ border-radius: 8px;
1842
+ transition: color 0.2s ease, background 0.2s ease;
1843
+ }
1844
+
1845
+ .link-btn:hover {
1846
+ color: #0ea5e9;
1847
+ background: rgba(14, 165, 233, 0.16);
1848
+ }
1849
+
1850
+
1851
+ .sources-block,
1852
+ .summary-block {
1853
+ position: relative;
1854
+ margin-top: 16px;
1855
+ padding: 18px;
1856
+ border-radius: 18px;
1857
+ background: rgba(255, 255, 255, 0.94);
1858
+ border: 1px solid rgba(148, 163, 184, 0.18);
1859
+ box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
1860
+ }
1861
+
1862
+ .sources-history {
1863
+ margin-top: 16px;
1864
+ display: flex;
1865
+ flex-direction: column;
1866
+ gap: 10px;
1867
+ }
1868
+
1869
+ .sources-history h4 {
1870
+ margin: 0;
1871
+ color: #1f2937;
1872
+ font-size: 14px;
1873
+ letter-spacing: 0.01em;
1874
+ }
1875
+
1876
+ .history-list {
1877
+ display: flex;
1878
+ flex-direction: column;
1879
+ gap: 10px;
1880
+ }
1881
+
1882
+ .history-list details {
1883
+ background: rgba(248, 250, 252, 0.95);
1884
+ border: 1px solid rgba(148, 163, 184, 0.24);
1885
+ border-radius: 14px;
1886
+ padding: 12px 16px;
1887
+ color: #1f2937;
1888
+ transition: border-color 0.2s ease, background 0.2s ease;
1889
+ }
1890
+
1891
+ .history-list details[open] {
1892
+ background: rgba(224, 231, 255, 0.55);
1893
+ border-color: rgba(129, 140, 248, 0.4);
1894
+ }
1895
+
1896
+ .history-list summary {
1897
+ cursor: pointer;
1898
+ font-weight: 600;
1899
+ outline: none;
1900
+ list-style: none;
1901
+ display: flex;
1902
+ align-items: center;
1903
+ justify-content: space-between;
1904
+ }
1905
+
1906
+ .history-list summary::-webkit-details-marker {
1907
+ display: none;
1908
+ }
1909
+
1910
+ .history-list summary::after {
1911
+ content: "▾";
1912
+ margin-left: 6px;
1913
+ font-size: 12px;
1914
+ opacity: 0.7;
1915
+ transition: transform 0.2s ease;
1916
+ }
1917
+
1918
+ .history-list details[open] summary::after {
1919
+ transform: rotate(180deg);
1920
+ }
1921
+
1922
+ .block-highlight {
1923
+ animation: glow 1.2s ease;
1924
+ }
1925
+
1926
+ .sources-block h3,
1927
+ .summary-block h3 {
1928
+ margin: 0 0 14px;
1929
+ color: #1f2937;
1930
+ letter-spacing: 0.02em;
1931
+ }
1932
+
1933
+ .sources-list {
1934
+ list-style: none;
1935
+ margin: 0;
1936
+ padding: 0;
1937
+ display: flex;
1938
+ flex-direction: column;
1939
+ gap: 10px;
1940
+ }
1941
+
1942
+ .source-item {
1943
+ position: relative;
1944
+ display: inline-flex;
1945
+ flex-direction: column;
1946
+ gap: 6px;
1947
+ }
1948
+
1949
+ .source-link {
1950
+ color: #2563eb;
1951
+ text-decoration: none;
1952
+ font-weight: 600;
1953
+ letter-spacing: 0.01em;
1954
+ transition: color 0.2s ease;
1955
+ }
1956
+
1957
+ .source-link::after {
1958
+ content: " ↗";
1959
+ font-size: 12px;
1960
+ opacity: 0.6;
1961
+ }
1962
+
1963
+ .source-link:hover {
1964
+ color: #0f172a;
1965
+ }
1966
+
1967
+ .source-tooltip {
1968
+ display: none;
1969
+ position: absolute;
1970
+ bottom: calc(100% + 12px);
1971
+ left: 50%;
1972
+ transform: translateX(-50%);
1973
+ background: rgba(255, 255, 255, 0.98);
1974
+ color: #1f2937;
1975
+ padding: 14px 16px;
1976
+ border-radius: 16px;
1977
+ box-shadow: 0 18px 32px rgba(15, 23, 42, 0.18);
1978
+ width: min(420px, 90vw);
1979
+ z-index: 20;
1980
+ border: 1px solid rgba(148, 163, 184, 0.24);
1981
+ }
1982
+
1983
+ .source-tooltip::after {
1984
+ content: "";
1985
+ position: absolute;
1986
+ top: 100%;
1987
+ left: 50%;
1988
+ transform: translateX(-50%);
1989
+ border-width: 10px;
1990
+ border-style: solid;
1991
+ border-color: rgba(255, 255, 255, 0.98) transparent transparent transparent;
1992
+ }
1993
+
1994
+ .source-tooltip::before {
1995
+ content: "";
1996
+ position: absolute;
1997
+ bottom: -12px;
1998
+ left: 50%;
1999
+ transform: translateX(-50%);
2000
+ border-width: 12px 10px 0 10px;
2001
+ border-style: solid;
2002
+ border-color: rgba(255, 255, 255, 0.98) transparent transparent transparent;
2003
+ filter: drop-shadow(0 -2px 4px rgba(15, 23, 42, 0.12));
2004
+ }
2005
+
2006
+ .source-tooltip p {
2007
+ margin: 0 0 8px;
2008
+ font-size: 13px;
2009
+ line-height: 1.6;
2010
+ }
2011
+
2012
+ .source-tooltip p:last-child {
2013
+ margin-bottom: 0;
2014
+ }
2015
+
2016
+ .muted-text {
2017
+ color: #64748b;
2018
+ }
2019
+
2020
+ .source-item:hover .source-tooltip,
2021
+ .source-item:focus-within .source-tooltip {
2022
+ display: block;
2023
+ }
2024
+
2025
+ .hint.muted {
2026
+ color: #64748b;
2027
+ }
2028
+
2029
+ @keyframes float {
2030
+ 0% {
2031
+ transform: translate3d(0, 0, 0) rotate(0deg);
2032
+ }
2033
+ 50% {
2034
+ transform: translate3d(10%, 6%, 0) rotate(3deg);
2035
+ }
2036
+ 100% {
2037
+ transform: translate3d(0, 0, 0) rotate(0deg);
2038
+ }
2039
+ }
2040
+
2041
+ @keyframes spin {
2042
+ to {
2043
+ transform: rotate(360deg);
2044
+ }
2045
+ }
2046
+
2047
+ @keyframes pulse {
2048
+ 0%,
2049
+ 100% {
2050
+ transform: scale(1);
2051
+ opacity: 1;
2052
+ }
2053
+ 50% {
2054
+ transform: scale(1.3);
2055
+ opacity: 0.5;
2056
+ }
2057
+ }
2058
+
2059
+ @keyframes glow {
2060
+ 0% {
2061
+ box-shadow: 0 0 0 rgba(59, 130, 246, 0.3);
2062
+ border-color: rgba(59, 130, 246, 0.5);
2063
+ }
2064
+ 100% {
2065
+ box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.12);
2066
+ border-color: rgba(148, 163, 184, 0.2);
2067
+ }
2068
+ }
2069
+
2070
+ @media (max-width: 960px) {
2071
+ .app-shell {
2072
+ padding: 56px 16px;
2073
+ }
2074
+
2075
+ .layout {
2076
+ flex-direction: column;
2077
+ align-items: stretch;
2078
+ }
2079
+
2080
+ .panel {
2081
+ padding: 22px;
2082
+ }
2083
+
2084
+ .panel-form,
2085
+ .panel-result {
2086
+ max-width: none;
2087
+ }
2088
+
2089
+ .status-bar {
2090
+ flex-direction: column;
2091
+ align-items: flex-start;
2092
+ }
2093
+
2094
+ .status-main,
2095
+ .status-controls {
2096
+ width: 100%;
2097
+ }
2098
+
2099
+ .status-controls {
2100
+ justify-content: flex-start;
2101
+ }
2102
+ }
2103
+
2104
+ @media (max-width: 600px) {
2105
+ .options {
2106
+ flex-direction: column;
2107
+ }
2108
+
2109
+ .status-meta {
2110
+ font-size: 12px;
2111
+ }
2112
+
2113
+ .panel-head {
2114
+ flex-direction: column;
2115
+ align-items: flex-start;
2116
+ }
2117
+
2118
+ .panel-form h1 {
2119
+ font-size: 24px;
2120
+ }
2121
+ }
2122
+
2123
+ /* Sidebar styles */
2124
+ .sidebar {
2125
+ width: 400px;
2126
+ min-width: 400px;
2127
+ height: 100vh;
2128
+ background: rgba(255, 255, 255, 0.98);
2129
+ border-right: 1px solid rgba(148, 163, 184, 0.2);
2130
+ padding: 32px 24px;
2131
+ display: flex;
2132
+ flex-direction: column;
2133
+ gap: 24px;
2134
+ overflow-y: auto;
2135
+ box-shadow: 4px 0 24px rgba(15, 23, 42, 0.08);
2136
+ }
2137
+
2138
+ .sidebar-header {
2139
+ display: flex;
2140
+ flex-direction: column;
2141
+ gap: 16px;
2142
+ }
2143
+
2144
+ .sidebar-header h2 {
2145
+ font-size: 24px;
2146
+ font-weight: 700;
2147
+ margin: 0;
2148
+ color: #1f2937;
2149
+ }
2150
+
2151
+ .back-btn {
2152
+ display: flex;
2153
+ align-items: center;
2154
+ gap: 8px;
2155
+ padding: 10px 16px;
2156
+ background: transparent;
2157
+ border: 1px solid rgba(148, 163, 184, 0.3);
2158
+ border-radius: 12px;
2159
+ color: #64748b;
2160
+ font-size: 14px;
2161
+ font-weight: 500;
2162
+ cursor: pointer;
2163
+ transition: all 0.2s ease;
2164
+ width: fit-content;
2165
+ }
2166
+
2167
+ .back-btn:hover:not(:disabled) {
2168
+ background: rgba(59, 130, 246, 0.1);
2169
+ border-color: #3b82f6;
2170
+ color: #3b82f6;
2171
+ }
2172
+
2173
+ .back-btn:disabled {
2174
+ opacity: 0.5;
2175
+ cursor: not-allowed;
2176
+ }
2177
+
2178
+ .research-info {
2179
+ flex: 1;
2180
+ display: flex;
2181
+ flex-direction: column;
2182
+ gap: 20px;
2183
+ }
2184
+
2185
+ .info-item {
2186
+ display: flex;
2187
+ flex-direction: column;
2188
+ gap: 8px;
2189
+ }
2190
+
2191
+ .info-item label {
2192
+ font-size: 12px;
2193
+ font-weight: 600;
2194
+ text-transform: uppercase;
2195
+ letter-spacing: 0.5px;
2196
+ color: #64748b;
2197
+ }
2198
+
2199
+ .info-item p {
2200
+ margin: 0;
2201
+ font-size: 14px;
2202
+ color: #1f2937;
2203
+ line-height: 1.6;
2204
+ }
2205
+
2206
+ .topic-display {
2207
+ font-size: 16px !important;
2208
+ font-weight: 600;
2209
+ color: #0f172a !important;
2210
+ padding: 12px;
2211
+ background: rgba(59, 130, 246, 0.05);
2212
+ border-radius: 8px;
2213
+ border-left: 3px solid #3b82f6;
2214
+ }
2215
+
2216
+ .progress-bar {
2217
+ width: 100%;
2218
+ height: 8px;
2219
+ background: rgba(148, 163, 184, 0.2);
2220
+ border-radius: 4px;
2221
+ overflow: hidden;
2222
+ }
2223
+
2224
+ .progress-fill {
2225
+ height: 100%;
2226
+ background: linear-gradient(90deg, #3b82f6, #8b5cf6);
2227
+ border-radius: 4px;
2228
+ transition: width 0.5s ease;
2229
+ }
2230
+
2231
+ .progress-text {
2232
+ font-size: 13px !important;
2233
+ color: #64748b !important;
2234
+ font-weight: 500;
2235
+ }
2236
+
2237
+ .sidebar-actions {
2238
+ display: flex;
2239
+ flex-direction: column;
2240
+ gap: 12px;
2241
+ padding-top: 16px;
2242
+ border-top: 1px solid rgba(148, 163, 184, 0.2);
2243
+ }
2244
+
2245
+ .new-research-btn {
2246
+ display: flex;
2247
+ align-items: center;
2248
+ justify-content: center;
2249
+ gap: 8px;
2250
+ padding: 14px 20px;
2251
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
2252
+ border: none;
2253
+ border-radius: 12px;
2254
+ color: white;
2255
+ font-size: 15px;
2256
+ font-weight: 600;
2257
+ cursor: pointer;
2258
+ transition: all 0.3s ease;
2259
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
2260
+ }
2261
+
2262
+ .new-research-btn:hover {
2263
+ transform: translateY(-2px);
2264
+ box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
2265
+ }
2266
+
2267
+ .new-research-btn:active {
2268
+ transform: translateY(0);
2269
+ }
2270
+
2271
+ /* Fullscreen result panel styles */
2272
+ .layout-fullscreen .panel-result {
2273
+ flex: 1;
2274
+ height: 100vh;
2275
+ border-radius: 0;
2276
+ border: none;
2277
+ overflow-y: auto;
2278
+ max-width: none;
2279
+ }
2280
+
2281
+ @media (max-width: 1024px) {
2282
+ .sidebar {
2283
+ width: 320px;
2284
+ min-width: 320px;
2285
+ }
2286
+ }
2287
+
2288
+ @media (max-width: 768px) {
2289
+ .layout-fullscreen {
2290
+ flex-direction: column;
2291
+ }
2292
+
2293
+ .sidebar {
2294
+ width: 100%;
2295
+ min-width: 100%;
2296
+ height: auto;
2297
+ max-height: 40vh;
2298
+ }
2299
+
2300
+ .layout-fullscreen .panel-result {
2301
+ height: 60vh;
2302
+ }
2303
+ }
2304
+ </style>
frontend/src/env.d.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ interface ImportMetaEnv {
2
+ readonly VITE_API_BASE_URL?: string;
3
+ }
4
+
5
+ interface ImportMeta {
6
+ readonly env: ImportMetaEnv;
7
+ }
frontend/src/main.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { createApp } from "vue";
2
+ import App from "./App.vue";
3
+
4
+ import "./style.css";
5
+
6
+ createApp(App).mount("#app");
frontend/src/services/api.ts ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const baseURL =
2
+ import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
3
+
4
+ export interface ResearchRequest {
5
+ topic: string;
6
+ search_api?: string;
7
+ }
8
+
9
+ export interface ResearchStreamEvent {
10
+ type: string;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ export interface StreamOptions {
15
+ signal?: AbortSignal;
16
+ }
17
+
18
+ export async function runResearchStream(
19
+ payload: ResearchRequest,
20
+ onEvent: (event: ResearchStreamEvent) => void,
21
+ options: StreamOptions = {}
22
+ ): Promise<void> {
23
+ const response = await fetch(`${baseURL}/research/stream`, {
24
+ method: "POST",
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ Accept: "text/event-stream"
28
+ },
29
+ body: JSON.stringify(payload),
30
+ signal: options.signal
31
+ });
32
+
33
+ if (!response.ok) {
34
+ const errorText = await response.text().catch(() => "");
35
+ throw new Error(
36
+ errorText || `Research request failed, status code: ${response.status}`
37
+ );
38
+ }
39
+
40
+ const body = response.body;
41
+ if (!body) {
42
+ throw new Error("Browser does not support streaming responses, unable to get research progress");
43
+ }
44
+
45
+ const reader = body.getReader();
46
+ const decoder = new TextDecoder("utf-8");
47
+ let buffer = "";
48
+
49
+ while (true) {
50
+ const { value, done } = await reader.read();
51
+ buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
52
+
53
+ let boundary = buffer.indexOf("\n\n");
54
+ while (boundary !== -1) {
55
+ const rawEvent = buffer.slice(0, boundary).trim();
56
+ buffer = buffer.slice(boundary + 2);
57
+
58
+ if (rawEvent.startsWith("data:")) {
59
+ const dataPayload = rawEvent.slice(5).trim();
60
+ if (dataPayload) {
61
+ try {
62
+ const event = JSON.parse(dataPayload) as ResearchStreamEvent;
63
+ onEvent(event);
64
+
65
+ if (event.type === "error" || event.type === "done") {
66
+ return;
67
+ }
68
+ } catch (error) {
69
+ console.error("Failed to parse streaming event:", error, dataPayload);
70
+ }
71
+ }
72
+ }
73
+
74
+ boundary = buffer.indexOf("\n\n");
75
+ }
76
+
77
+ if (done) {
78
+ // Handle possible trailing events
79
+ if (buffer.trim()) {
80
+ const rawEvent = buffer.trim();
81
+ if (rawEvent.startsWith("data:")) {
82
+ const dataPayload = rawEvent.slice(5).trim();
83
+ if (dataPayload) {
84
+ try {
85
+ const event = JSON.parse(dataPayload) as ResearchStreamEvent;
86
+ onEvent(event);
87
+ } catch (error) {
88
+ console.error("Failed to parse streaming event:", error, dataPayload);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ break;
94
+ }
95
+ }
96
+ }
frontend/src/style.css ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ color: #1a1a1a;
3
+ background-color: #f6f6f6;
4
+ font-family: "Helvetica Neue", Arial, sans-serif;
5
+ line-height: 1.5;
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ }
15
+
16
+ #app {
17
+ min-height: 100vh;
18
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "Node",
7
+ "strict": true,
8
+ "jsx": "preserve",
9
+ "esModuleInterop": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "lib": ["ESNext", "DOM"],
13
+ "skipLibCheck": true,
14
+ "types": ["vite/client"],
15
+ "baseUrl": "./",
16
+ "paths": {
17
+ "@/*": ["src/*"]
18
+ }
19
+ },
20
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
21
+ "references": [{ "path": "./tsconfig.node.json" }]
22
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node",
6
+ "allowSyntheticDefaultImports": true
7
+ },
8
+ "include": ["vite.config.ts"]
9
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import vue from "@vitejs/plugin-vue";
3
+
4
+ export default defineConfig({
5
+ plugins: [vue()],
6
+ server: {
7
+ port: 5174
8
+ }
9
+ });