mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-03 06:12:14 +08:00
chore: 移除所有 Citations 相关逻辑,为后续重构做准备
- Backend: 删除 lead_agent / general_purpose 中的 citations_format 与引用相关 reminder;artifacts 下载不再对 markdown 做 citation 清洗,统一走 FileResponse,保留 Response 用于二进制 inline - Frontend: 删除 core/citations 模块、inline-citation、safe-citation-content;新增 MarkdownContent 仅做 Markdown 渲染;消息/artifact 预览与复制均使用原始 content - i18n: 移除 citations 命名空间(loadingCitations、loadingCitationsWithCount) - 技能与 demo: 措辞改为 references,demo 数据去掉 <citations> 块 - 文档: 更新 CLAUDE/AGENTS/README 描述,新增按文件 diff 的代码变更总结 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -156,7 +156,7 @@ FastAPI application on port 8001 with health check at `GET /health`.
|
|||||||
| **Skills** (`/api/skills`) | `GET /` - list skills; `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive |
|
| **Skills** (`/api/skills`) | `GET /` - list skills; `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive |
|
||||||
| **Memory** (`/api/memory`) | `GET /` - memory data; `POST /reload` - force reload; `GET /config` - config; `GET /status` - config + data |
|
| **Memory** (`/api/memory`) | `GET /` - memory data; `POST /reload` - force reload; `GET /config` - config; `GET /status` - config + data |
|
||||||
| **Uploads** (`/api/threads/{id}/uploads`) | `POST /` - upload files (auto-converts PDF/PPT/Excel/Word); `GET /list` - list; `DELETE /{filename}` - delete |
|
| **Uploads** (`/api/threads/{id}/uploads`) | `POST /` - upload files (auto-converts PDF/PPT/Excel/Word); `GET /list` - list; `DELETE /{filename}` - delete |
|
||||||
| **Artifacts** (`/api/threads/{id}/artifacts`) | `GET /{path}` - serve artifacts; `?download=true` for download with citation removal |
|
| **Artifacts** (`/api/threads/{id}/artifacts`) | `GET /{path}` - serve artifacts; `?download=true` for file download |
|
||||||
|
|
||||||
Proxied through nginx: `/api/langgraph/*` → LangGraph, all other `/api/*` → Gateway.
|
Proxied through nginx: `/api/langgraph/*` → LangGraph, all other `/api/*` → Gateway.
|
||||||
|
|
||||||
|
|||||||
@@ -240,34 +240,8 @@ You have access to skills that provide optimized workflows for specific tasks. E
|
|||||||
- Action-Oriented: Focus on delivering results, not explaining processes
|
- Action-Oriented: Focus on delivering results, not explaining processes
|
||||||
</response_style>
|
</response_style>
|
||||||
|
|
||||||
<citations_format>
|
|
||||||
After web_search, ALWAYS include citations in your output:
|
|
||||||
|
|
||||||
1. Start with a `<citations>` block in JSONL format listing all sources
|
|
||||||
2. In content, use FULL markdown link format: [Short Title](full_url)
|
|
||||||
|
|
||||||
**CRITICAL - Citation Link Format:**
|
|
||||||
- CORRECT: `[TechCrunch](https://techcrunch.com/ai-trends)` - full markdown link with URL
|
|
||||||
- WRONG: `[arXiv:2502.19166]` - missing URL, will NOT render as link
|
|
||||||
- WRONG: `[Source]` - missing URL, will NOT render as link
|
|
||||||
|
|
||||||
**Rules:**
|
|
||||||
- Every citation MUST be a complete markdown link with URL: `[Title](https://...)`
|
|
||||||
- Write content naturally, add citation link at end of sentence/paragraph
|
|
||||||
- NEVER use bare brackets like `[arXiv:xxx]` or `[Source]` without URL
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
<citations>
|
|
||||||
{{"id": "cite-1", "title": "AI Trends 2026", "url": "https://techcrunch.com/ai-trends", "snippet": "Tech industry predictions"}}
|
|
||||||
{{"id": "cite-2", "title": "OpenAI Research", "url": "https://openai.com/research", "snippet": "Latest AI research developments"}}
|
|
||||||
</citations>
|
|
||||||
The key AI trends for 2026 include enhanced reasoning capabilities and multimodal integration [TechCrunch](https://techcrunch.com/ai-trends). Recent breakthroughs in language models have also accelerated progress [OpenAI](https://openai.com/research).
|
|
||||||
</citations_format>
|
|
||||||
|
|
||||||
|
|
||||||
<critical_reminders>
|
<critical_reminders>
|
||||||
- **Clarification First**: ALWAYS clarify unclear/missing/ambiguous requirements BEFORE starting work - never assume or guess
|
- **Clarification First**: ALWAYS clarify unclear/missing/ambiguous requirements BEFORE starting work - never assume or guess
|
||||||
- **Web search citations**: When you use web_search (or synthesize subagent results that used it), you MUST output the `<citations>` block and [Title](url) links as specified in citations_format so citations display for the user.
|
|
||||||
{subagent_reminder}- Skill First: Always load the relevant skill before starting **complex** tasks.
|
{subagent_reminder}- Skill First: Always load the relevant skill before starting **complex** tasks.
|
||||||
- Progressive Loading: Load resources incrementally as referenced in skills
|
- Progressive Loading: Load resources incrementally as referenced in skills
|
||||||
- Output Files: Final deliverables must be in `/mnt/user-data/outputs`
|
- Output Files: Final deliverables must be in `/mnt/user-data/outputs`
|
||||||
@@ -341,7 +315,6 @@ def apply_prompt_template(subagent_enabled: bool = False) -> str:
|
|||||||
# Add subagent reminder to critical_reminders if enabled
|
# Add subagent reminder to critical_reminders if enabled
|
||||||
subagent_reminder = (
|
subagent_reminder = (
|
||||||
"- **Orchestrator Mode**: You are a task orchestrator - decompose complex tasks into parallel sub-tasks and launch multiple subagents simultaneously. Synthesize results, don't execute directly.\n"
|
"- **Orchestrator Mode**: You are a task orchestrator - decompose complex tasks into parallel sub-tasks and launch multiple subagents simultaneously. Synthesize results, don't execute directly.\n"
|
||||||
"- **Citations when synthesizing**: When you synthesize subagent results that used web search or cite sources, you MUST include a consolidated `<citations>` block (JSONL format) and use [Title](url) markdown links in your response so citations display correctly.\n"
|
|
||||||
if subagent_enabled
|
if subagent_enabled
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import json
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import re
|
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request, Response
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse
|
from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse, Response
|
||||||
|
|
||||||
from src.gateway.path_utils import resolve_thread_virtual_path
|
from src.gateway.path_utils import resolve_thread_virtual_path
|
||||||
|
|
||||||
@@ -24,40 +22,6 @@ def is_text_file_by_content(path: Path, sample_size: int = 8192) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _extract_citation_urls(content: str) -> set[str]:
|
|
||||||
"""Extract URLs from <citations> JSONL blocks. Format must match frontend core/citations/utils.ts."""
|
|
||||||
urls: set[str] = set()
|
|
||||||
for match in re.finditer(r"<citations>([\s\S]*?)</citations>", content):
|
|
||||||
for line in match.group(1).split("\n"):
|
|
||||||
line = line.strip()
|
|
||||||
if line.startswith("{"):
|
|
||||||
try:
|
|
||||||
obj = json.loads(line)
|
|
||||||
if "url" in obj:
|
|
||||||
urls.add(obj["url"])
|
|
||||||
except (json.JSONDecodeError, ValueError):
|
|
||||||
pass
|
|
||||||
return urls
|
|
||||||
|
|
||||||
|
|
||||||
def remove_citations_block(content: str) -> str:
|
|
||||||
"""Remove ALL citations from markdown (blocks, [cite-N], and citation links). Used for downloads."""
|
|
||||||
if not content:
|
|
||||||
return content
|
|
||||||
|
|
||||||
citation_urls = _extract_citation_urls(content)
|
|
||||||
|
|
||||||
result = re.sub(r"<citations>[\s\S]*?</citations>", "", content)
|
|
||||||
if "<citations>" in result:
|
|
||||||
result = re.sub(r"<citations>[\s\S]*$", "", result)
|
|
||||||
result = re.sub(r"\[cite-\d+\]", "", result)
|
|
||||||
|
|
||||||
for url in citation_urls:
|
|
||||||
result = re.sub(rf"\[[^\]]+\]\({re.escape(url)}\)", "", result)
|
|
||||||
|
|
||||||
return re.sub(r"\n{3,}", "\n\n", result).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_file_from_skill_archive(zip_path: Path, internal_path: str) -> bytes | None:
|
def _extract_file_from_skill_archive(zip_path: Path, internal_path: str) -> bytes | None:
|
||||||
"""Extract a file from a .skill ZIP archive.
|
"""Extract a file from a .skill ZIP archive.
|
||||||
|
|
||||||
@@ -173,23 +137,8 @@ async def get_artifact(thread_id: str, path: str, request: Request) -> FileRespo
|
|||||||
# Encode filename for Content-Disposition header (RFC 5987)
|
# Encode filename for Content-Disposition header (RFC 5987)
|
||||||
encoded_filename = quote(actual_path.name)
|
encoded_filename = quote(actual_path.name)
|
||||||
|
|
||||||
# Check if this is a markdown file that might contain citations
|
|
||||||
is_markdown = mime_type == "text/markdown" or actual_path.suffix.lower() in [".md", ".markdown"]
|
|
||||||
|
|
||||||
# if `download` query parameter is true, return the file as a download
|
# if `download` query parameter is true, return the file as a download
|
||||||
if request.query_params.get("download"):
|
if request.query_params.get("download"):
|
||||||
# For markdown files, remove citations block before download
|
|
||||||
if is_markdown:
|
|
||||||
content = actual_path.read_text()
|
|
||||||
clean_content = remove_citations_block(content)
|
|
||||||
return Response(
|
|
||||||
content=clean_content.encode("utf-8"),
|
|
||||||
media_type="text/markdown",
|
|
||||||
headers={
|
|
||||||
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
|
|
||||||
"Content-Type": "text/markdown; charset=utf-8"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return FileResponse(path=actual_path, filename=actual_path.name, media_type=mime_type, headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"})
|
return FileResponse(path=actual_path, filename=actual_path.name, media_type=mime_type, headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"})
|
||||||
|
|
||||||
if mime_type and mime_type == "text/html":
|
if mime_type and mime_type == "text/html":
|
||||||
|
|||||||
@@ -24,21 +24,10 @@ Do NOT use for simple, single-step operations.""",
|
|||||||
- Do NOT ask for clarification - work with the information provided
|
- Do NOT ask for clarification - work with the information provided
|
||||||
</guidelines>
|
</guidelines>
|
||||||
|
|
||||||
<citations_format>
|
|
||||||
If you used web_search (or similar) and cite sources, ALWAYS include citations in your output:
|
|
||||||
1. Start with a `<citations>` block in JSONL format listing all sources (one JSON object per line)
|
|
||||||
2. In content, use FULL markdown link format: [Short Title](full_url)
|
|
||||||
- Every citation MUST be a complete markdown link with URL: [Title](https://...)
|
|
||||||
- Example block:
|
|
||||||
<citations>
|
|
||||||
{"id": "cite-1", "title": "...", "url": "https://...", "snippet": "..."}
|
|
||||||
</citations>
|
|
||||||
</citations_format>
|
|
||||||
|
|
||||||
<output_format>
|
<output_format>
|
||||||
When you complete the task, provide:
|
When you complete the task, provide:
|
||||||
1. A brief summary of what was accomplished
|
1. A brief summary of what was accomplished
|
||||||
2. Key findings or results (with citation links when from web search)
|
2. Key findings or results
|
||||||
3. Any relevant file paths, data, or artifacts created
|
3. Any relevant file paths, data, or artifacts created
|
||||||
4. Issues encountered (if any)
|
4. Issues encountered (if any)
|
||||||
</output_format>
|
</output_format>
|
||||||
|
|||||||
939
docs/CODE_CHANGE_SUMMARY_BY_FILE.md
Normal file
939
docs/CODE_CHANGE_SUMMARY_BY_FILE.md
Normal file
@@ -0,0 +1,939 @@
|
|||||||
|
# 代码更改总结(按文件 diff,细到每一行)
|
||||||
|
|
||||||
|
基于 `git diff HEAD` 的完整 diff,按文件列出所有变更。删除/新增文件单独说明。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、后端
|
||||||
|
|
||||||
|
### 1. `backend/CLAUDE.md`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -156,7 +156,7 @@ FastAPI application on port 8001 with health check at `GET /health`.
|
||||||
|
| **Skills** (`/api/skills`) | `GET /` - list skills; `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive |
|
||||||
|
| **Memory** (`/api/memory`) | `GET /` - memory data; `POST /reload` - force reload; `GET /config` - config; `GET /status` - config + data |
|
||||||
|
| **Uploads** (`/api/threads/{id}/uploads`) | `POST /` - upload files (auto-converts PDF/PPT/Excel/Word); `GET /list` - list; `DELETE /{filename}` - delete |
|
||||||
|
-| **Artifacts** (`/api/threads/{id}/artifacts`) | `GET /{path}` - serve artifacts; `?download=true` for download with citation removal |
|
||||||
|
+| **Artifacts** (`/api/threads/{id}/artifacts`) | `GET /{path}` - serve artifacts; `?download=true` for file download |
|
||||||
|
|
||||||
|
Proxied through nginx: `/api/langgraph/*` → LangGraph, all other `/api/*` → Gateway.
|
||||||
|
```
|
||||||
|
|
||||||
|
- **第 159 行**:表格中 Artifacts 描述由「download with citation removal」改为「file download」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `backend/src/agents/lead_agent/prompt.py`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -240,34 +240,8 @@ You have access to skills that provide optimized workflows for specific tasks. E
|
||||||
|
- Action-Oriented: Focus on delivering results, not explaining processes
|
||||||
|
</response_style>
|
||||||
|
|
||||||
|
-<citations_format>
|
||||||
|
-After web_search, ALWAYS include citations in your output:
|
||||||
|
-
|
||||||
|
-1. Start with a `<citations>` block in JSONL format listing all sources
|
||||||
|
-2. In content, use FULL markdown link format: [Short Title](full_url)
|
||||||
|
-
|
||||||
|
-**CRITICAL - Citation Link Format:**
|
||||||
|
-- CORRECT: `[TechCrunch](https://techcrunch.com/ai-trends)` - full markdown link with URL
|
||||||
|
-- WRONG: `[arXiv:2502.19166]` - missing URL, will NOT render as link
|
||||||
|
-- WRONG: `[Source]` - missing URL, will NOT render as link
|
||||||
|
-
|
||||||
|
-**Rules:**
|
||||||
|
-- Every citation MUST be a complete markdown link with URL: `[Title](https://...)`
|
||||||
|
-- Write content naturally, add citation link at end of sentence/paragraph
|
||||||
|
-- NEVER use bare brackets like `[arXiv:xxx]` or `[Source]` without URL
|
||||||
|
-
|
||||||
|
-**Example:**
|
||||||
|
-<citations>
|
||||||
|
-{{"id": "cite-1", "title": "AI Trends 2026", "url": "https://techcrunch.com/ai-trends", "snippet": "Tech industry predictions"}}
|
||||||
|
-{{"id": "cite-2", "title": "OpenAI Research", "url": "https://openai.com/research", "snippet": "Latest AI research developments"}}
|
||||||
|
-</citations>
|
||||||
|
-The key AI trends for 2026 include enhanced reasoning capabilities and multimodal integration [TechCrunch](https://techcrunch.com/ai-trends). Recent breakthroughs in language models have also accelerated progress [OpenAI](https://openai.com/research).
|
||||||
|
-</citations_format>
|
||||||
|
-
|
||||||
|
-
|
||||||
|
<critical_reminders>
|
||||||
|
- **Clarification First**: ALWAYS clarify unclear/missing/ambiguous requirements BEFORE starting work - never assume or guess
|
||||||
|
-- **Web search citations**: When you use web_search (or synthesize subagent results that used it), you MUST output the `<citations>` block and [Title](url) links as specified in citations_format so citations display for the user.
|
||||||
|
{subagent_reminder}- Skill First: Always load the relevant skill before starting **complex** tasks.
|
||||||
|
```
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -341,7 +315,6 @@ def apply_prompt_template(subagent_enabled: bool = False) -> str:
|
||||||
|
# Add subagent reminder to critical_reminders if enabled
|
||||||
|
subagent_reminder = (
|
||||||
|
"- **Orchestrator Mode**: You are a task orchestrator - decompose complex tasks into parallel sub-tasks and launch multiple subagents simultaneously. Synthesize results, don't execute directly.\n"
|
||||||
|
- "- **Citations when synthesizing**: When you synthesize subagent results that used web search or cite sources, you MUST include a consolidated `<citations>` block (JSONL format) and use [Title](url) markdown links in your response so citations display correctly.\n"
|
||||||
|
if subagent_enabled
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **删除**:`<citations_format>...</citations_format>` 整段(原约 243–266 行)、critical_reminders 中「Web search citations」一条、`apply_prompt_template` 中「Citations when synthesizing」一行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. `backend/src/gateway/routers/artifacts.py`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -1,12 +1,10 @@
|
||||||
|
-import json
|
||||||
|
import mimetypes
|
||||||
|
-import re
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
-from fastapi import APIRouter, HTTPException, Request, Response
|
||||||
|
-from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse
|
||||||
|
+from fastapi import APIRouter, HTTPException, Request
|
||||||
|
+from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse, Response
|
||||||
|
|
||||||
|
from src.gateway.path_utils import resolve_thread_virtual_path
|
||||||
|
```
|
||||||
|
|
||||||
|
- **第 1 行**:删除 `import json`。
|
||||||
|
- **第 3 行**:删除 `import re`。
|
||||||
|
- **第 6–7 行**:`fastapi` 中去掉 `Response`;`fastapi.responses` 中增加 `Response`(保留二进制 inline 返回用)。
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -24,40 +22,6 @@ def is_text_file_by_content(path: Path, sample_size: int = 8192) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
-def _extract_citation_urls(content: str) -> set[str]:
|
||||||
|
- """Extract URLs from <citations> JSONL blocks. Format must match frontend core/citations/utils.ts."""
|
||||||
|
- urls: set[str] = set()
|
||||||
|
- for match in re.finditer(r"<citations>([\s\S]*?)</citations>", content):
|
||||||
|
- for line in match.group(1).split("\n"):
|
||||||
|
- line = line.strip()
|
||||||
|
- if line.startswith("{"):
|
||||||
|
- try:
|
||||||
|
- obj = json.loads(line)
|
||||||
|
- if "url" in obj:
|
||||||
|
- urls.add(obj["url"])
|
||||||
|
- except (json.JSONDecodeError, ValueError):
|
||||||
|
- pass
|
||||||
|
- return urls
|
||||||
|
-
|
||||||
|
-
|
||||||
|
-def remove_citations_block(content: str) -> str:
|
||||||
|
- """Remove ALL citations from markdown (blocks, [cite-N], and citation links). Used for downloads."""
|
||||||
|
- if not content:
|
||||||
|
- return content
|
||||||
|
-
|
||||||
|
- citation_urls = _extract_citation_urls(content)
|
||||||
|
-
|
||||||
|
- result = re.sub(r"<citations>[\s\S]*?</citations>", "", content)
|
||||||
|
- if "<citations>" in result:
|
||||||
|
- result = re.sub(r"<citations>[\s\S]*$", "", result)
|
||||||
|
- result = re.sub(r"\[cite-\d+\]", "", result)
|
||||||
|
-
|
||||||
|
- for url in citation_urls:
|
||||||
|
- result = re.sub(rf"\[[^\]]+\]\({re.escape(url)}\)", "", result)
|
||||||
|
-
|
||||||
|
- return re.sub(r"\n{3,}", "\n\n", result).strip()
|
||||||
|
-
|
||||||
|
-
|
||||||
|
def _extract_file_from_skill_archive(zip_path: Path, internal_path: str) -> bytes | None:
|
||||||
|
```
|
||||||
|
|
||||||
|
- **删除**:`_extract_citation_urls`、`remove_citations_block` 两个函数(约 25–62 行)。
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -172,24 +136,9 @@ async def get_artifact(thread_id: str, path: str, request: Request) -> FileRespo
|
||||||
|
|
||||||
|
# Encode filename for Content-Disposition header (RFC 5987)
|
||||||
|
encoded_filename = quote(actual_path.name)
|
||||||
|
-
|
||||||
|
- # Check if this is a markdown file that might contain citations
|
||||||
|
- is_markdown = mime_type == "text/markdown" or actual_path.suffix.lower() in [".md", ".markdown"]
|
||||||
|
-
|
||||||
|
+
|
||||||
|
# if `download` query parameter is true, return the file as a download
|
||||||
|
if request.query_params.get("download"):
|
||||||
|
- # For markdown files, remove citations block before download
|
||||||
|
- if is_markdown:
|
||||||
|
- content = actual_path.read_text()
|
||||||
|
- clean_content = remove_citations_block(content)
|
||||||
|
- return Response(
|
||||||
|
- content=clean_content.encode("utf-8"),
|
||||||
|
- media_type="text/markdown",
|
||||||
|
- headers={
|
||||||
|
- "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
|
||||||
|
- "Content-Type": "text/markdown; charset=utf-8"
|
||||||
|
- }
|
||||||
|
- )
|
||||||
|
return FileResponse(path=actual_path, filename=actual_path.name, media_type=mime_type, headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"})
|
||||||
|
|
||||||
|
if mime_type and mime_type == "text/html":
|
||||||
|
```
|
||||||
|
|
||||||
|
- **删除**:`is_markdown` 判断及「markdown 时读文件 + remove_citations_block + Response」分支;download 时统一走 `FileResponse`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. `backend/src/subagents/builtins/general_purpose.py`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -24,21 +24,10 @@ Do NOT use for simple, single-step operations.""",
|
||||||
|
- Do NOT ask for clarification - work with the information provided
|
||||||
|
</guidelines>
|
||||||
|
|
||||||
|
-<citations_format>
|
||||||
|
-If you used web_search (or similar) and cite sources, ALWAYS include citations in your output:
|
||||||
|
-1. Start with a `<citations>` block in JSONL format listing all sources (one JSON object per line)
|
||||||
|
-2. In content, use FULL markdown link format: [Short Title](full_url)
|
||||||
|
-- Every citation MUST be a complete markdown link with URL: [Title](https://...)
|
||||||
|
-- Example block:
|
||||||
|
-<citations>
|
||||||
|
-{"id": "cite-1", "title": "...", "url": "https://...", "snippet": "..."}
|
||||||
|
-</citations>
|
||||||
|
-</citations_format>
|
||||||
|
-
|
||||||
|
<output_format>
|
||||||
|
When you complete the task, provide:
|
||||||
|
1. A brief summary of what was accomplished
|
||||||
|
-2. Key findings or results (with citation links when from web search)
|
||||||
|
+2. Key findings or results
|
||||||
|
3. Any relevant file paths, data, or artifacts created
|
||||||
|
4. Issues encountered (if any)
|
||||||
|
</output_format>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **删除**:`<citations_format>...</citations_format>` 整段。
|
||||||
|
- **第 40 行**:第 2 条由「Key findings or results (with citation links when from web search)」改为「Key findings or results」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、前端文档与工具
|
||||||
|
|
||||||
|
### 5. `frontend/AGENTS.md`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -49,7 +49,6 @@ src/
|
||||||
|
├── core/ # Core business logic
|
||||||
|
│ ├── api/ # API client & data fetching
|
||||||
|
│ ├── artifacts/ # Artifact management
|
||||||
|
-│ ├── citations/ # Citation handling
|
||||||
|
│ ├── config/ # App configuration
|
||||||
|
│ ├── i18n/ # Internationalization
|
||||||
|
```
|
||||||
|
|
||||||
|
- **第 52 行**:删除目录树中的 `citations/` 一行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. `frontend/CLAUDE.md`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -30,7 +30,7 @@ Frontend (Next.js) ──▶ LangGraph SDK ──▶ LangGraph Backend (lead_age
|
||||||
|
└── Tools & Skills
|
||||||
|
```
|
||||||
|
|
||||||
|
-The frontend is a stateful chat application. Users create **threads** (conversations), send messages, and receive streamed AI responses. The backend orchestrates agents that can produce **artifacts** (files/code), **todos**, and **citations**.
|
||||||
|
+The frontend is a stateful chat application. Users create **threads** (conversations), send messages, and receive streamed AI responses. The backend orchestrates agents that can produce **artifacts** (files/code) and **todos**.
|
||||||
|
|
||||||
|
### Source Layout (`src/`)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **第 33 行**:「and **citations**」删除。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. `frontend/README.md`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -89,7 +89,6 @@ src/
|
||||||
|
├── core/ # Core business logic
|
||||||
|
│ ├── api/ # API client & data fetching
|
||||||
|
│ ├── artifacts/ # Artifact management
|
||||||
|
-│ ├── citations/ # Citation handling
|
||||||
|
│ ├── config/ # App configuration
|
||||||
|
│ ├── i18n/ # Internationalization
|
||||||
|
```
|
||||||
|
|
||||||
|
- **第 92 行**:删除目录树中的 `citations/` 一行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. `frontend/src/lib/utils.ts`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -8,5 +8,5 @@ export function cn(...inputs: ClassValue[]) {
|
||||||
|
/** Shared class for external links (underline by default). */
|
||||||
|
export const externalLinkClass =
|
||||||
|
"text-primary underline underline-offset-2 hover:no-underline";
|
||||||
|
-/** For streaming / loading state when link may be a citation (no underline). */
|
||||||
|
+/** Link style without underline by default (e.g. for streaming/loading). */
|
||||||
|
export const externalLinkClassNoUnderline = "text-primary hover:underline";
|
||||||
|
```
|
||||||
|
|
||||||
|
- **第 11 行**:仅注释修改,导出值未变。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、前端组件
|
||||||
|
|
||||||
|
### 9. `frontend/src/components/workspace/artifacts/artifact-file-detail.tsx`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -8,7 +8,6 @@ import {
|
||||||
|
SquareArrowOutUpRightIcon,
|
||||||
|
XIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
-import * as React from "react";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
...
|
||||||
|
@@ -21,7 +20,6 @@ import (
|
||||||
|
ArtifactHeader,
|
||||||
|
ArtifactTitle,
|
||||||
|
} from "@/components/ai-elements/artifact";
|
||||||
|
-import { createCitationMarkdownComponents } from "@/components/ai-elements/inline-citation";
|
||||||
|
import { Select, SelectItem } from "@/components/ui/select";
|
||||||
|
...
|
||||||
|
@@ -33,12 +31,6 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
|
import { CodeEditor } from "@/components/workspace/code-editor";
|
||||||
|
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||||
|
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||||
|
-import type { Citation } from "@/core/citations";
|
||||||
|
-import {
|
||||||
|
- contentWithoutCitationsFromParsed,
|
||||||
|
- removeAllCitations,
|
||||||
|
- useParsedCitations,
|
||||||
|
-} from "@/core/citations";
|
||||||
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
...
|
||||||
|
@@ -48,9 +40,6 @@ import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { Tooltip } from "../tooltip";
|
||||||
|
|
||||||
|
-import { SafeCitationContent } from "../messages/safe-citation-content";
|
||||||
|
-import { useThread } from "../messages/context";
|
||||||
|
-
|
||||||
|
import { useArtifacts } from "./context";
|
||||||
|
```
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -92,22 +81,13 @@ export function ArtifactFileDetail({
|
||||||
|
const previewable = useMemo(() => {
|
||||||
|
return (language === "html" && !isWriteFile) || language === "markdown";
|
||||||
|
}, [isWriteFile, language]);
|
||||||
|
- const { thread } = useThread();
|
||||||
|
const { content } = useArtifactContent({
|
||||||
|
threadId,
|
||||||
|
filepath: filepathFromProps,
|
||||||
|
enabled: isCodeFile && !isWriteFile,
|
||||||
|
});
|
||||||
|
|
||||||
|
- const parsed = useParsedCitations(
|
||||||
|
- language === "markdown" ? (content ?? "") : "",
|
||||||
|
- );
|
||||||
|
- const cleanContent =
|
||||||
|
- language === "markdown" && content ? parsed.cleanContent : (content ?? "");
|
||||||
|
- const contentWithoutCitations =
|
||||||
|
- language === "markdown" && content
|
||||||
|
- ? contentWithoutCitationsFromParsed(parsed)
|
||||||
|
- : (content ?? "");
|
||||||
|
+ const displayContent = content ?? "";
|
||||||
|
|
||||||
|
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
||||||
|
```
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -219,7 +199,7 @@ export function ArtifactFileDetail({
|
||||||
|
disabled={!content}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
- await navigator.clipboard.writeText(contentWithoutCitations ?? "");
|
||||||
|
+ await navigator.clipboard.writeText(displayContent ?? "");
|
||||||
|
toast.success(t.clipboard.copiedToClipboard);
|
||||||
|
...
|
||||||
|
@@ -255,27 +235,17 @@ export function ArtifactFileDetail({
|
||||||
|
viewMode === "preview" &&
|
||||||
|
language === "markdown" &&
|
||||||
|
content && (
|
||||||
|
- <SafeCitationContent
|
||||||
|
- content={content}
|
||||||
|
- isLoading={thread.isLoading}
|
||||||
|
- rehypePlugins={streamdownPlugins.rehypePlugins}
|
||||||
|
- className="flex size-full items-center justify-center p-4 my-0"
|
||||||
|
- renderBody={(p) => (
|
||||||
|
- <ArtifactFilePreview
|
||||||
|
- filepath={filepath}
|
||||||
|
- threadId={threadId}
|
||||||
|
- content={content}
|
||||||
|
- language={language ?? "text"}
|
||||||
|
- cleanContent={p.cleanContent}
|
||||||
|
- citationMap={p.citationMap}
|
||||||
|
- />
|
||||||
|
- )}
|
||||||
|
+ <ArtifactFilePreview
|
||||||
|
+ filepath={filepath}
|
||||||
|
+ threadId={threadId}
|
||||||
|
+ content={displayContent}
|
||||||
|
+ language={language ?? "text"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isCodeFile && viewMode === "code" && (
|
||||||
|
<CodeEditor
|
||||||
|
className="size-full resize-none rounded-none border-none"
|
||||||
|
- value={cleanContent ?? ""}
|
||||||
|
+ value={displayContent ?? ""}
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -295,29 +265,17 @@ export function ArtifactFilePreview({
|
||||||
|
threadId,
|
||||||
|
content,
|
||||||
|
language,
|
||||||
|
- cleanContent,
|
||||||
|
- citationMap,
|
||||||
|
}: {
|
||||||
|
filepath: string;
|
||||||
|
threadId: string;
|
||||||
|
content: string;
|
||||||
|
language: string;
|
||||||
|
- cleanContent: string;
|
||||||
|
- citationMap: Map<string, Citation>;
|
||||||
|
}) {
|
||||||
|
if (language === "markdown") {
|
||||||
|
- const components = createCitationMarkdownComponents({
|
||||||
|
- citationMap,
|
||||||
|
- syntheticExternal: true,
|
||||||
|
- });
|
||||||
|
return (
|
||||||
|
<div className="size-full px-4">
|
||||||
|
- <Streamdown
|
||||||
|
- className="size-full"
|
||||||
|
- {...streamdownPlugins}
|
||||||
|
- components={components}
|
||||||
|
- >
|
||||||
|
- {cleanContent ?? ""}
|
||||||
|
+ <Streamdown className="size-full" {...streamdownPlugins}>
|
||||||
|
+ {content ?? ""}
|
||||||
|
</Streamdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- 删除:React 命名空间、inline-citation、core/citations、SafeCitationContent、useThread;parsed/cleanContent/contentWithoutCitations 及引用解析逻辑。
|
||||||
|
- 新增:`displayContent = content ?? ""`;预览与复制、CodeEditor 均使用 `displayContent`;`ArtifactFilePreview` 仅保留 `content`/`language` 等,去掉 `cleanContent`/`citationMap` 与 `createCitationMarkdownComponents`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. `frontend/src/components/workspace/messages/message-group.tsx`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -39,9 +39,7 @@ import { useArtifacts } from "../artifacts";
|
||||||
|
import { FlipDisplay } from "../flip-display";
|
||||||
|
import { Tooltip } from "../tooltip";
|
||||||
|
|
||||||
|
-import { useThread } from "./context";
|
||||||
|
-
|
||||||
|
-import { SafeCitationContent } from "./safe-citation-content";
|
||||||
|
+import { MarkdownContent } from "./markdown-content";
|
||||||
|
|
||||||
|
export function MessageGroup({
|
||||||
|
```
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -120,7 +118,7 @@ export function MessageGroup({
|
||||||
|
<ChainOfThoughtStep
|
||||||
|
key={step.id}
|
||||||
|
label={
|
||||||
|
- <SafeCitationContent
|
||||||
|
+ <MarkdownContent
|
||||||
|
content={step.reasoning ?? ""}
|
||||||
|
isLoading={isLoading}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
@@ -128,12 +126,7 @@ export function MessageGroup({
|
||||||
|
}
|
||||||
|
></ChainOfThoughtStep>
|
||||||
|
) : (
|
||||||
|
- <ToolCall
|
||||||
|
- key={step.id}
|
||||||
|
- {...step}
|
||||||
|
- isLoading={isLoading}
|
||||||
|
- rehypePlugins={rehypePlugins}
|
||||||
|
- />
|
||||||
|
+ <ToolCall key={step.id} {...step} isLoading={isLoading} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
{lastToolCallStep && (
|
||||||
|
@@ -143,7 +136,6 @@ export function MessageGroup({
|
||||||
|
{...lastToolCallStep}
|
||||||
|
isLast={true}
|
||||||
|
isLoading={isLoading}
|
||||||
|
- rehypePlugins={rehypePlugins}
|
||||||
|
/>
|
||||||
|
</FlipDisplay>
|
||||||
|
)}
|
||||||
|
@@ -178,7 +170,7 @@ export function MessageGroup({
|
||||||
|
<ChainOfThoughtStep
|
||||||
|
key={lastReasoningStep.id}
|
||||||
|
label={
|
||||||
|
- <SafeCitationContent
|
||||||
|
+ <MarkdownContent
|
||||||
|
content={lastReasoningStep.reasoning ?? ""}
|
||||||
|
isLoading={isLoading}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
@@ -201,7 +193,6 @@ function ToolCall({
|
||||||
|
result,
|
||||||
|
isLast = false,
|
||||||
|
isLoading = false,
|
||||||
|
- rehypePlugins,
|
||||||
|
}: {
|
||||||
|
id?: string;
|
||||||
|
messageId?: string;
|
||||||
|
@@ -210,15 +201,10 @@ function ToolCall({
|
||||||
|
result?: string | Record<string, unknown>;
|
||||||
|
isLast?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
- rehypePlugins: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
|
||||||
|
}) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { setOpen, autoOpen, autoSelect, selectedArtifact, select } =
|
||||||
|
useArtifacts();
|
||||||
|
- const { thread } = useThread();
|
||||||
|
- const threadIsLoading = thread.isLoading;
|
||||||
|
-
|
||||||
|
- const fileContent = typeof args.content === "string" ? args.content : "";
|
||||||
|
|
||||||
|
if (name === "web_search") {
|
||||||
|
```
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -364,42 +350,27 @@ function ToolCall({
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
- const isMarkdown =
|
||||||
|
- path?.toLowerCase().endsWith(".md") ||
|
||||||
|
- path?.toLowerCase().endsWith(".markdown");
|
||||||
|
-
|
||||||
|
return (
|
||||||
|
- <>
|
||||||
|
- <ChainOfThoughtStep
|
||||||
|
- key={id}
|
||||||
|
- className="cursor-pointer"
|
||||||
|
- label={description}
|
||||||
|
- icon={NotebookPenIcon}
|
||||||
|
- onClick={() => {
|
||||||
|
- select(
|
||||||
|
- new URL(
|
||||||
|
- `write-file:${path}?message_id=${messageId}&tool_call_id=${id}`,
|
||||||
|
- ).toString(),
|
||||||
|
- );
|
||||||
|
- setOpen(true);
|
||||||
|
- }}
|
||||||
|
- >
|
||||||
|
- {path && (
|
||||||
|
- <ChainOfThoughtSearchResult className="cursor-pointer">
|
||||||
|
- {path}
|
||||||
|
- </ChainOfThoughtSearchResult>
|
||||||
|
- )}
|
||||||
|
- </ChainOfThoughtStep>
|
||||||
|
- {isMarkdown && (
|
||||||
|
- <SafeCitationContent
|
||||||
|
- content={fileContent}
|
||||||
|
- isLoading={threadIsLoading && isLast}
|
||||||
|
- rehypePlugins={rehypePlugins}
|
||||||
|
- loadingOnly
|
||||||
|
- className="mt-2 ml-8"
|
||||||
|
- />
|
||||||
|
+ <ChainOfThoughtStep
|
||||||
|
+ key={id}
|
||||||
|
+ className="cursor-pointer"
|
||||||
|
+ label={description}
|
||||||
|
+ icon={NotebookPenIcon}
|
||||||
|
+ onClick={() => {
|
||||||
|
+ select(
|
||||||
|
+ new URL(
|
||||||
|
+ `write-file:${path}?message_id=${messageId}&tool_call_id=${id}`,
|
||||||
|
+ ).toString(),
|
||||||
|
+ );
|
||||||
|
+ setOpen(true);
|
||||||
|
+ }}
|
||||||
|
+ >
|
||||||
|
+ {path && (
|
||||||
|
+ <ChainOfThoughtSearchResult className="cursor-pointer">
|
||||||
|
+ {path}
|
||||||
|
+ </ChainOfThoughtSearchResult>
|
||||||
|
)}
|
||||||
|
- </>
|
||||||
|
+ </ChainOfThoughtStep>
|
||||||
|
);
|
||||||
|
} else if (name === "bash") {
|
||||||
|
```
|
||||||
|
|
||||||
|
- 两处 `SafeCitationContent` → `MarkdownContent`;ToolCall 去掉 `rehypePlugins` 及内部 `useThread`/`fileContent`;write_file 分支去掉 markdown 预览块(`isMarkdown` + `SafeCitationContent`),仅保留 `ChainOfThoughtStep` + path。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. `frontend/src/components/workspace/messages/message-list-item.tsx`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -12,7 +12,6 @@ import {
|
||||||
|
} from "@/components/ai-elements/message";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { resolveArtifactURL } from "@/core/artifacts/utils";
|
||||||
|
-import { removeAllCitations } from "@/core/citations";
|
||||||
|
import {
|
||||||
|
extractContentFromMessage,
|
||||||
|
extractReasoningContentFromMessage,
|
||||||
|
@@ -24,7 +23,7 @@ import { humanMessagePlugins } from "@/core/streamdown";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { CopyButton } from "../copy-button";
|
||||||
|
-import { SafeCitationContent } from "./safe-citation-content";
|
||||||
|
+import { MarkdownContent } from "./markdown-content";
|
||||||
|
...
|
||||||
|
@@ -54,11 +53,11 @@ export function MessageListItem({
|
||||||
|
>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<CopyButton
|
||||||
|
- clipboardData={removeAllCitations(
|
||||||
|
+ clipboardData={
|
||||||
|
extractContentFromMessage(message) ??
|
||||||
|
extractReasoningContentFromMessage(message) ??
|
||||||
|
""
|
||||||
|
- )}
|
||||||
|
+ }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</MessageToolbar>
|
||||||
|
@@ -154,7 +153,7 @@ function MessageContent_({
|
||||||
|
return (
|
||||||
|
<AIElementMessageContent className={className}>
|
||||||
|
{filesList}
|
||||||
|
- <SafeCitationContent
|
||||||
|
+ <MarkdownContent
|
||||||
|
content={contentToParse}
|
||||||
|
isLoading={isLoading}
|
||||||
|
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: "html" }]]}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 删除 `removeAllCitations` 与 `SafeCitationContent` 引用;复制改为原始内容;渲染改为 `MarkdownContent`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. `frontend/src/components/workspace/messages/message-list.tsx`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -26,7 +26,7 @@ import { StreamingIndicator } from "../streaming-indicator";
|
||||||
|
|
||||||
|
import { MessageGroup } from "./message-group";
|
||||||
|
import { MessageListItem } from "./message-list-item";
|
||||||
|
-import { SafeCitationContent } from "./safe-citation-content";
|
||||||
|
+import { MarkdownContent } from "./markdown-content";
|
||||||
|
import { MessageListSkeleton } from "./skeleton";
|
||||||
|
...
|
||||||
|
@@ -69,7 +69,7 @@ export function MessageList({
|
||||||
|
const message = group.messages[0];
|
||||||
|
if (message && hasContent(message)) {
|
||||||
|
return (
|
||||||
|
- <SafeCitationContent
|
||||||
|
+ <MarkdownContent
|
||||||
|
key={group.id}
|
||||||
|
content={extractContentFromMessage(message)}
|
||||||
|
isLoading={thread.isLoading}
|
||||||
|
@@ -89,7 +89,7 @@ export function MessageList({
|
||||||
|
return (
|
||||||
|
<div className="w-full" key={group.id}>
|
||||||
|
{group.messages[0] && hasContent(group.messages[0]) && (
|
||||||
|
- <SafeCitationContent
|
||||||
|
+ <MarkdownContent
|
||||||
|
content={extractContentFromMessage(group.messages[0])}
|
||||||
|
isLoading={thread.isLoading}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 三处:import 与两处渲染均由 `SafeCitationContent` 改为 `MarkdownContent`,props 不变。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. `frontend/src/components/workspace/messages/subtask-card.tsx`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -29,7 +29,7 @@ import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { FlipDisplay } from "../flip-display";
|
||||||
|
|
||||||
|
-import { SafeCitationContent } from "./safe-citation-content";
|
||||||
|
+import { MarkdownContent } from "./markdown-content";
|
||||||
|
...
|
||||||
|
@@ -153,7 +153,7 @@ export function SubtaskCard({
|
||||||
|
<ChainOfThoughtStep
|
||||||
|
label={
|
||||||
|
task.result ? (
|
||||||
|
- <SafeCitationContent
|
||||||
|
+ <MarkdownContent
|
||||||
|
content={task.result}
|
||||||
|
isLoading={false}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
```
|
||||||
|
|
||||||
|
- import 与一处渲染:`SafeCitationContent` → `MarkdownContent`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14. 新增 `frontend/src/components/workspace/messages/markdown-content.tsx`
|
||||||
|
|
||||||
|
(当前工作区新增,未在 git 中)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ImgHTMLAttributes } from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MessageResponse,
|
||||||
|
type MessageResponseProps,
|
||||||
|
} from "@/components/ai-elements/message";
|
||||||
|
import { streamdownPlugins } from "@/core/streamdown";
|
||||||
|
|
||||||
|
export type MarkdownContentProps = {
|
||||||
|
content: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
rehypePlugins: MessageResponseProps["rehypePlugins"];
|
||||||
|
className?: string;
|
||||||
|
remarkPlugins?: MessageResponseProps["remarkPlugins"];
|
||||||
|
isHuman?: boolean;
|
||||||
|
img?: (props: ImgHTMLAttributes<HTMLImageElement> & { threadId?: string; maxWidth?: string }) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Renders markdown content. */
|
||||||
|
export function MarkdownContent({
|
||||||
|
content,
|
||||||
|
rehypePlugins,
|
||||||
|
className,
|
||||||
|
remarkPlugins = streamdownPlugins.remarkPlugins,
|
||||||
|
img,
|
||||||
|
}: MarkdownContentProps) {
|
||||||
|
if (!content) return null;
|
||||||
|
const components = img ? { img } : undefined;
|
||||||
|
return (
|
||||||
|
<MessageResponse
|
||||||
|
className={className}
|
||||||
|
remarkPlugins={remarkPlugins}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
components={components}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</MessageResponse>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 纯 Markdown 渲染组件,无引用解析或 loading 占位逻辑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 15. 删除 `frontend/src/components/workspace/messages/safe-citation-content.tsx`
|
||||||
|
|
||||||
|
- 原约 85 行;提供引用解析、loading、renderBody/loadingOnly、cleanContent/citationMap。已由 `MarkdownContent` 替代,整文件删除。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 16. 删除 `frontend/src/components/ai-elements/inline-citation.tsx`
|
||||||
|
|
||||||
|
- 原约 289 行;提供 `createCitationMarkdownComponents` 等,用于将 `[cite-N]`/URL 渲染为可点击引用。仅被 artifact 预览使用,已移除后整文件删除。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、前端 core
|
||||||
|
|
||||||
|
### 17. 删除 `frontend/src/core/citations/index.ts`
|
||||||
|
|
||||||
|
- 原 13 行,导出:`contentWithoutCitationsFromParsed`、`extractDomainFromUrl`、`isExternalUrl`、`parseCitations`、`removeAllCitations`、`shouldShowCitationLoading`、`syntheticCitationFromLink`、`useParsedCitations`、类型 `Citation`/`ParseCitationsResult`/`UseParsedCitationsResult`。整文件删除。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 18. 删除 `frontend/src/core/citations/use-parsed-citations.ts`
|
||||||
|
|
||||||
|
- 原 28 行,`useParsedCitations(content)` 与 `UseParsedCitationsResult`。整文件删除。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 19. 删除 `frontend/src/core/citations/utils.ts`
|
||||||
|
|
||||||
|
- 原 226 行,解析 `<citations>`/`[cite-N]`、buildCitationMap、removeAllCitations、contentWithoutCitationsFromParsed 等。整文件删除。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 20. `frontend/src/core/i18n/locales/types.ts`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -115,12 +115,6 @@ export interface Translations {
|
||||||
|
startConversation: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
- // Citations
|
||||||
|
- citations: {
|
||||||
|
- loadingCitations: string;
|
||||||
|
- loadingCitationsWithCount: (count: number) => string;
|
||||||
|
- };
|
||||||
|
-
|
||||||
|
// Chats
|
||||||
|
chats: {
|
||||||
|
```
|
||||||
|
|
||||||
|
- 删除 `Translations.citations` 及其两个字段。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 21. `frontend/src/core/i18n/locales/zh-CN.ts`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -164,12 +164,6 @@ export const zhCN: Translations = {
|
||||||
|
startConversation: "开始新的对话以查看消息",
|
||||||
|
},
|
||||||
|
|
||||||
|
- // Citations
|
||||||
|
- citations: {
|
||||||
|
- loadingCitations: "正在整理引用...",
|
||||||
|
- loadingCitationsWithCount: (count: number) => `正在整理 ${count} 个引用...`,
|
||||||
|
- },
|
||||||
|
-
|
||||||
|
// Chats
|
||||||
|
chats: {
|
||||||
|
```
|
||||||
|
|
||||||
|
- 删除 `citations` 命名空间。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 22. `frontend/src/core/i18n/locales/en-US.ts`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -167,13 +167,6 @@ export const enUS: Translations = {
|
||||||
|
startConversation: "Start a conversation to see messages here",
|
||||||
|
},
|
||||||
|
|
||||||
|
- // Citations
|
||||||
|
- citations: {
|
||||||
|
- loadingCitations: "Organizing citations...",
|
||||||
|
- loadingCitationsWithCount: (count: number) =>
|
||||||
|
- `Organizing ${count} citation${count === 1 ? "" : "s"}...`,
|
||||||
|
- },
|
||||||
|
-
|
||||||
|
// Chats
|
||||||
|
chats: {
|
||||||
|
```
|
||||||
|
|
||||||
|
- 删除 `citations` 命名空间。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、技能与 Demo
|
||||||
|
|
||||||
|
### 23. `skills/public/github-deep-research/SKILL.md`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -147,5 +147,5 @@ Save report as: `research_{topic}_{YYYYMMDD}.md`
|
||||||
|
3. **Triangulate claims** - 2+ independent sources
|
||||||
|
4. **Note conflicting info** - Don't hide contradictions
|
||||||
|
5. **Distinguish fact vs opinion** - Label speculation clearly
|
||||||
|
-6. **Cite inline** - Reference sources near claims
|
||||||
|
+6. **Reference sources** - Add source references near claims where applicable
|
||||||
|
7. **Update as you go** - Don't wait until end to synthesize
|
||||||
|
```
|
||||||
|
|
||||||
|
- 第 150 行:一条措辞修改。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 24. `skills/public/market-analysis/SKILL.md`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -15,7 +15,7 @@ This skill generates professional, consulting-grade market analysis reports in M
|
||||||
|
- Follow the **"Visual Anchor → Data Contrast → Integrated Analysis"** flow per sub-chapter
|
||||||
|
- Produce insights following the **"Data → User Psychology → Strategy Implication"** chain
|
||||||
|
- Embed pre-generated charts and construct comparison tables
|
||||||
|
-- Generate inline citations formatted per **GB/T 7714-2015** standards
|
||||||
|
+- Include references formatted per **GB/T 7714-2015** where applicable
|
||||||
|
- Output reports entirely in Chinese with professional consulting tone
|
||||||
|
...
|
||||||
|
@@ -36,7 +36,7 @@ The skill expects the following inputs from the upstream agentic workflow:
|
||||||
|
| **Analysis Framework Outline** | Defines the logic flow and general topics for the report | Yes |
|
||||||
|
| **Data Summary** | The source of truth containing raw numbers and metrics | Yes |
|
||||||
|
| **Chart Files** | Local file paths for pre-generated chart images | Yes |
|
||||||
|
-| **External Search Findings** | URLs and summaries for inline citations | Optional |
|
||||||
|
+| **External Search Findings** | URLs and summaries for inline references | Optional |
|
||||||
|
...
|
||||||
|
@@ -87,7 +87,7 @@ The report **MUST NOT** stop after the Conclusion — it **MUST** include Refere
|
||||||
|
- **Tone**: McKinsey/BCG — Authoritative, Objective, Professional
|
||||||
|
- **Language**: All headings and content strictly in **Chinese**
|
||||||
|
- **Number Formatting**: Use English commas for thousands separators (`1,000` not `1,000`)
|
||||||
|
-- **Data Citation**: **Bold** important viewpoints and key numbers
|
||||||
|
+- **Data emphasis**: **Bold** important viewpoints and key numbers
|
||||||
|
...
|
||||||
|
@@ -109,11 +109,9 @@ Every insight must connect **Data → User Psychology → Strategy Implication**
|
||||||
|
treating male audiences only as a secondary gift-giving segment."
|
||||||
|
```
|
||||||
|
|
||||||
|
-### Citations & References
|
||||||
|
-- **Inline**: Use `[\[Index\]](URL)` format (e.g., `[\[1\]](https://example.com)`)
|
||||||
|
-- **Placement**: Append citations at the end of sentences using information from External Search Findings
|
||||||
|
-- **Index Assignment**: Sequential starting from **1** based on order of appearance
|
||||||
|
-- **References Section**: Formatted strictly per **GB/T 7714-2015**
|
||||||
|
+### References
|
||||||
|
+- **Inline**: Use markdown links for sources (e.g. `[Source Title](URL)`) when using External Search Findings
|
||||||
|
+- **References section**: Formatted strictly per **GB/T 7714-2015**
|
||||||
|
...
|
||||||
|
@@ -183,7 +181,7 @@ Before considering the report complete, verify:
|
||||||
|
- [ ] All headings are in Chinese with proper numbering (no "Chapter/Part/Section")
|
||||||
|
- [ ] Charts are embedded with `` syntax
|
||||||
|
- [ ] Numbers use English commas for thousands separators
|
||||||
|
-- [ ] Inline citations use `[\[N\]](URL)` format
|
||||||
|
+- [ ] Inline references use markdown links where applicable
|
||||||
|
- [ ] References section follows GB/T 7714-2015
|
||||||
|
```
|
||||||
|
|
||||||
|
- 多处:核心能力、输入表、Data Citation、Citations & References 小节与检查项,改为「references / 引用」表述并去掉 `[\[N\]](URL)` 格式要求。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 25. `frontend/public/demo/threads/.../user-data/outputs/research_deerflow_20260201.md`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -1,12 +1,3 @@
|
||||||
|
-<citations>
|
||||||
|
-{"id": "cite-1", "title": "DeerFlow GitHub Repository", "url": "https://github.com/bytedance/deer-flow", "snippet": "..."}
|
||||||
|
-...(共 7 条 JSONL)
|
||||||
|
-</citations>
|
||||||
|
# DeerFlow Deep Research Report
|
||||||
|
|
||||||
|
- **Research Date:** 2026-02-01
|
||||||
|
```
|
||||||
|
|
||||||
|
- 删除文件开头的 `<citations>...</citations>` 整块(9 行),正文从 `# DeerFlow Deep Research Report` 开始。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 26. `frontend/public/demo/threads/.../thread.json`
|
||||||
|
|
||||||
|
- **主要变更**:某条 `write_file` 的 `args.content` 中,将原来的「`<citations>...\n</citations>\n# DeerFlow Deep Research Report\n\n...`」改为「`# DeerFlow Deep Research Report\n\n...`」,即去掉 `<citations>...</citations>` 块,保留其后全文。
|
||||||
|
- **其他**:一处 `present_files` 的 `filepaths` 由单行数组改为多行格式;文件末尾增加/统一换行。
|
||||||
|
- 消息顺序、结构及其他字段未改。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、统计
|
||||||
|
|
||||||
|
| 项目 | 数量 |
|
||||||
|
|------|------|
|
||||||
|
| 修改文件 | 18 |
|
||||||
|
| 新增文件 | 1(markdown-content.tsx) |
|
||||||
|
| 删除文件 | 5(safe-citation-content.tsx, inline-citation.tsx, core/citations/* 共 3 个) |
|
||||||
|
| 总行数变化 | +62 / -894(diff stat) |
|
||||||
|
|
||||||
|
以上为按文件、细到每一行 diff 的代码更改总结。
|
||||||
@@ -49,7 +49,6 @@ src/
|
|||||||
├── core/ # Core business logic
|
├── core/ # Core business logic
|
||||||
│ ├── api/ # API client & data fetching
|
│ ├── api/ # API client & data fetching
|
||||||
│ ├── artifacts/ # Artifact management
|
│ ├── artifacts/ # Artifact management
|
||||||
│ ├── citations/ # Citation handling
|
|
||||||
│ ├── config/ # App configuration
|
│ ├── config/ # App configuration
|
||||||
│ ├── i18n/ # Internationalization
|
│ ├── i18n/ # Internationalization
|
||||||
│ ├── mcp/ # MCP integration
|
│ ├── mcp/ # MCP integration
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Frontend (Next.js) ──▶ LangGraph SDK ──▶ LangGraph Backend (lead_age
|
|||||||
└── Tools & Skills
|
└── Tools & Skills
|
||||||
```
|
```
|
||||||
|
|
||||||
The frontend is a stateful chat application. Users create **threads** (conversations), send messages, and receive streamed AI responses. The backend orchestrates agents that can produce **artifacts** (files/code), **todos**, and **citations**.
|
The frontend is a stateful chat application. Users create **threads** (conversations), send messages, and receive streamed AI responses. The backend orchestrates agents that can produce **artifacts** (files/code) and **todos**.
|
||||||
|
|
||||||
### Source Layout (`src/`)
|
### Source Layout (`src/`)
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ src/
|
|||||||
├── core/ # Core business logic
|
├── core/ # Core business logic
|
||||||
│ ├── api/ # API client & data fetching
|
│ ├── api/ # API client & data fetching
|
||||||
│ ├── artifacts/ # Artifact management
|
│ ├── artifacts/ # Artifact management
|
||||||
│ ├── citations/ # Citation handling
|
|
||||||
│ ├── config/ # App configuration
|
│ ├── config/ # App configuration
|
||||||
│ ├── i18n/ # Internationalization
|
│ ├── i18n/ # Internationalization
|
||||||
│ ├── mcp/ # MCP integration
|
│ ├── mcp/ # MCP integration
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,12 +1,3 @@
|
|||||||
<citations>
|
|
||||||
{"id": "cite-1", "title": "DeerFlow GitHub Repository", "url": "https://github.com/bytedance/deer-flow", "snippet": "Community-driven Deep Research framework combining language models with tools like web search, crawling, and Python execution"}
|
|
||||||
{"id": "cite-2", "title": "Create Your Own Deep Research Agent with DeerFlow", "url": "https://thesequence.substack.com/p/the-sequence-engineering-661-create", "snippet": "Open-source multi-agent research automation framework developed by ByteDance"}
|
|
||||||
{"id": "cite-3", "title": "DeerFlow: A Game-Changer for Automated Research and Content Creation", "url": "https://medium.com/@mingyang.heaven/deerflow-a-game-changer-for-automated-research-and-content-creation-83612f683e7a", "snippet": "Open-source multi-agent framework revolutionizing research and content creation"}
|
|
||||||
{"id": "cite-4", "title": "ByteDance DeerFlow - (Deep Research Agents with a LOCAL LLM!)", "url": "https://www.youtube.com/watch?v=Ui0ovCVDYGs", "snippet": "Video demonstration of DeerFlow capabilities and local LLM integration"}
|
|
||||||
{"id": "cite-5", "title": "DeerFlow Official Website", "url": "https://deerflow.tech/", "snippet": "Official platform showcasing DeerFlow features, case studies, and architecture"}
|
|
||||||
{"id": "cite-6", "title": "Navigating the Landscape of Deep Research Frameworks", "url": "https://www.oreateai.com/blog/navigating-the-landscape-of-deep-research-frameworks-a-comprehensive-comparison/0dc13e48eb8c756650112842c8d1a184", "snippet": "Comparative analysis of deep research frameworks including DeerFlow"}
|
|
||||||
{"id": "cite-7", "title": "DeerFlow: Multi-Agent AI For Research Automation 2025", "url": "https://firexcore.com/blog/what-is-deerflow/", "snippet": "Overview of DeerFlow features and capabilities for research automation"}
|
|
||||||
</citations>
|
|
||||||
# DeerFlow Deep Research Report
|
# DeerFlow Deep Research Report
|
||||||
|
|
||||||
- **Research Date:** 2026-02-01
|
- **Research Date:** 2026-02-01
|
||||||
|
|||||||
@@ -1,289 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
HoverCard,
|
|
||||||
HoverCardContent,
|
|
||||||
HoverCardTrigger,
|
|
||||||
} from "@/components/ui/hover-card";
|
|
||||||
import {
|
|
||||||
cn,
|
|
||||||
externalLinkClass,
|
|
||||||
externalLinkClassNoUnderline,
|
|
||||||
} from "@/lib/utils";
|
|
||||||
import { ExternalLinkIcon } from "lucide-react";
|
|
||||||
import {
|
|
||||||
type AnchorHTMLAttributes,
|
|
||||||
type ComponentProps,
|
|
||||||
type ImgHTMLAttributes,
|
|
||||||
type ReactElement,
|
|
||||||
type ReactNode,
|
|
||||||
Children,
|
|
||||||
} from "react";
|
|
||||||
import type { Citation } from "@/core/citations";
|
|
||||||
import {
|
|
||||||
extractDomainFromUrl,
|
|
||||||
isExternalUrl,
|
|
||||||
syntheticCitationFromLink,
|
|
||||||
} from "@/core/citations";
|
|
||||||
import { Shimmer } from "./shimmer";
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
|
|
||||||
type InlineCitationCardProps = ComponentProps<typeof HoverCard>;
|
|
||||||
|
|
||||||
const InlineCitationCard = (props: InlineCitationCardProps) => (
|
|
||||||
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const InlineCitationCardBody = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ComponentProps<"div">) => (
|
|
||||||
<HoverCardContent className={cn("relative w-80 p-0", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const InlineCitationSource = ({
|
|
||||||
title,
|
|
||||||
url,
|
|
||||||
description,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ComponentProps<"div"> & {
|
|
||||||
title?: string;
|
|
||||||
url?: string;
|
|
||||||
description?: string;
|
|
||||||
}) => (
|
|
||||||
<div className={cn("space-y-1", className)} {...props}>
|
|
||||||
{title && (
|
|
||||||
<h4 className="truncate font-medium text-sm leading-tight">{title}</h4>
|
|
||||||
)}
|
|
||||||
{url && (
|
|
||||||
<p className="truncate break-all text-muted-foreground text-xs">{url}</p>
|
|
||||||
)}
|
|
||||||
{description && (
|
|
||||||
<p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared CitationLink component that renders a citation as a hover card badge
|
|
||||||
* Used across message-list-item, artifact-file-detail, and message-group
|
|
||||||
*
|
|
||||||
* When citation is provided, displays title and snippet from the citation.
|
|
||||||
* When citation is omitted, falls back to displaying the domain name extracted from href.
|
|
||||||
*/
|
|
||||||
export type CitationLinkProps = {
|
|
||||||
citation?: Citation;
|
|
||||||
href: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CitationLink = ({
|
|
||||||
citation,
|
|
||||||
href,
|
|
||||||
children,
|
|
||||||
}: CitationLinkProps) => {
|
|
||||||
const domain = extractDomainFromUrl(href);
|
|
||||||
|
|
||||||
// Priority: citation.title > children (if meaningful) > domain
|
|
||||||
// - citation.title: from parsed <citations> block, most accurate
|
|
||||||
// - children: from markdown link text [Text](url), used when no citation data
|
|
||||||
// - domain: fallback when both above are unavailable
|
|
||||||
// Skip children if it's a generic placeholder like "Source"
|
|
||||||
const childrenText = typeof children === "string" ? children : null;
|
|
||||||
const isGenericText = childrenText === "Source" || childrenText === "来源";
|
|
||||||
const displayText = citation?.title || (!isGenericText && childrenText) || domain;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InlineCitationCard>
|
|
||||||
<HoverCardTrigger asChild>
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className="hover:bg-secondary/80 mx-0.5 cursor-pointer gap-1 rounded-full px-2 py-0.5 text-xs font-normal"
|
|
||||||
>
|
|
||||||
{displayText}
|
|
||||||
<ExternalLinkIcon className="size-3" />
|
|
||||||
</Badge>
|
|
||||||
</a>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
<InlineCitationCardBody>
|
|
||||||
<div className="p-3">
|
|
||||||
<InlineCitationSource
|
|
||||||
title={citation?.title || domain}
|
|
||||||
url={href}
|
|
||||||
description={citation?.snippet}
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary mt-2 inline-flex items-center gap-1 text-xs hover:underline"
|
|
||||||
>
|
|
||||||
Visit source
|
|
||||||
<ExternalLinkIcon className="size-3" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</InlineCitationCardBody>
|
|
||||||
</InlineCitationCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a link with optional citation badge. Use in markdown components (message + artifact).
|
|
||||||
* - citationMap: URL -> Citation; links in map render as CitationLink.
|
|
||||||
* - isHuman: when true, never render as CitationLink (plain link).
|
|
||||||
* - isLoadingCitations: when true and not human, non-citation links use no-underline style.
|
|
||||||
* - syntheticExternal: when true, external URLs not in citationMap render as CitationLink with synthetic citation.
|
|
||||||
*/
|
|
||||||
export type CitationAwareLinkProps = ComponentProps<"a"> & {
|
|
||||||
citationMap: Map<string, Citation>;
|
|
||||||
isHuman?: boolean;
|
|
||||||
isLoadingCitations?: boolean;
|
|
||||||
syntheticExternal?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CitationAwareLink = ({
|
|
||||||
href,
|
|
||||||
children,
|
|
||||||
citationMap,
|
|
||||||
isHuman = false,
|
|
||||||
isLoadingCitations = false,
|
|
||||||
syntheticExternal = false,
|
|
||||||
className,
|
|
||||||
...rest
|
|
||||||
}: CitationAwareLinkProps) => {
|
|
||||||
if (!href) return <span>{children}</span>;
|
|
||||||
|
|
||||||
const citation = citationMap.get(href);
|
|
||||||
|
|
||||||
if (citation && !isHuman) {
|
|
||||||
return (
|
|
||||||
<CitationLink citation={citation} href={href}>
|
|
||||||
{children}
|
|
||||||
</CitationLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syntheticExternal && isExternalUrl(href)) {
|
|
||||||
const linkText =
|
|
||||||
typeof children === "string"
|
|
||||||
? children
|
|
||||||
: String(Children.toArray(children).join("")).trim() || href;
|
|
||||||
return (
|
|
||||||
<CitationLink
|
|
||||||
citation={syntheticCitationFromLink(href, linkText)}
|
|
||||||
href={href}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CitationLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const noUnderline = !isHuman && isLoadingCitations;
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={cn(noUnderline ? externalLinkClassNoUnderline : externalLinkClass, className)}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options for creating markdown components that render links as citations.
|
|
||||||
* Used by message list (all modes: Flash/Thinking/Pro/Ultra), artifact preview, and CoT.
|
|
||||||
*/
|
|
||||||
export type CreateCitationMarkdownComponentsOptions = {
|
|
||||||
citationMap: Map<string, Citation>;
|
|
||||||
isHuman?: boolean;
|
|
||||||
isLoadingCitations?: boolean;
|
|
||||||
syntheticExternal?: boolean;
|
|
||||||
/** Optional custom img component (e.g. MessageImage with threadId). Omit for artifact. */
|
|
||||||
img?: (props: ImgHTMLAttributes<HTMLImageElement> & { threadId?: string; maxWidth?: string }) => ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create markdown `components` (a, optional img) that use CitationAwareLink.
|
|
||||||
* Reused across message-list-item (all modes), artifact-file-detail, and any CoT markdown.
|
|
||||||
*/
|
|
||||||
export function createCitationMarkdownComponents(
|
|
||||||
options: CreateCitationMarkdownComponentsOptions,
|
|
||||||
): {
|
|
||||||
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => ReactElement;
|
|
||||||
img?: (props: ImgHTMLAttributes<HTMLImageElement> & { threadId?: string; maxWidth?: string }) => ReactNode;
|
|
||||||
} {
|
|
||||||
const {
|
|
||||||
citationMap,
|
|
||||||
isHuman = false,
|
|
||||||
isLoadingCitations = false,
|
|
||||||
syntheticExternal = false,
|
|
||||||
img,
|
|
||||||
} = options;
|
|
||||||
const a = (props: AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
|
||||||
<CitationAwareLink
|
|
||||||
{...props}
|
|
||||||
citationMap={citationMap}
|
|
||||||
isHuman={isHuman}
|
|
||||||
isLoadingCitations={isLoadingCitations}
|
|
||||||
syntheticExternal={syntheticExternal}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return img ? { a, img } : { a };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared CitationsLoadingIndicator component
|
|
||||||
* Used across message-list-item and message-group to show loading citations
|
|
||||||
*/
|
|
||||||
export type CitationsLoadingIndicatorProps = {
|
|
||||||
citations: Citation[];
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CitationsLoadingIndicator = ({
|
|
||||||
citations,
|
|
||||||
className,
|
|
||||||
}: CitationsLoadingIndicatorProps) => {
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("flex flex-col gap-2", className)}>
|
|
||||||
<Shimmer duration={2.5} className="text-sm">
|
|
||||||
{citations.length > 0
|
|
||||||
? t.citations.loadingCitationsWithCount(citations.length)
|
|
||||||
: t.citations.loadingCitations}
|
|
||||||
</Shimmer>
|
|
||||||
{citations.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{citations.map((citation) => (
|
|
||||||
<Badge
|
|
||||||
key={citation.id}
|
|
||||||
variant="secondary"
|
|
||||||
className="animate-fade-in gap-1 rounded-full px-2.5 py-1 text-xs font-normal"
|
|
||||||
>
|
|
||||||
<Shimmer duration={2} as="span">
|
|
||||||
{citation.title || extractDomainFromUrl(citation.url)}
|
|
||||||
</Shimmer>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
SquareArrowOutUpRightIcon,
|
SquareArrowOutUpRightIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import * as React from "react";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Streamdown } from "streamdown";
|
import { Streamdown } from "streamdown";
|
||||||
@@ -21,7 +20,6 @@ import {
|
|||||||
ArtifactHeader,
|
ArtifactHeader,
|
||||||
ArtifactTitle,
|
ArtifactTitle,
|
||||||
} from "@/components/ai-elements/artifact";
|
} from "@/components/ai-elements/artifact";
|
||||||
import { createCitationMarkdownComponents } from "@/components/ai-elements/inline-citation";
|
|
||||||
import { Select, SelectItem } from "@/components/ui/select";
|
import { Select, SelectItem } from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -33,12 +31,6 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|||||||
import { CodeEditor } from "@/components/workspace/code-editor";
|
import { CodeEditor } from "@/components/workspace/code-editor";
|
||||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||||
import type { Citation } from "@/core/citations";
|
|
||||||
import {
|
|
||||||
contentWithoutCitationsFromParsed,
|
|
||||||
removeAllCitations,
|
|
||||||
useParsedCitations,
|
|
||||||
} from "@/core/citations";
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { installSkill } from "@/core/skills/api";
|
import { installSkill } from "@/core/skills/api";
|
||||||
import { streamdownPlugins } from "@/core/streamdown";
|
import { streamdownPlugins } from "@/core/streamdown";
|
||||||
@@ -48,9 +40,6 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
import { Tooltip } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
|
|
||||||
import { SafeCitationContent } from "../messages/safe-citation-content";
|
|
||||||
import { useThread } from "../messages/context";
|
|
||||||
|
|
||||||
import { useArtifacts } from "./context";
|
import { useArtifacts } from "./context";
|
||||||
|
|
||||||
export function ArtifactFileDetail({
|
export function ArtifactFileDetail({
|
||||||
@@ -92,22 +81,13 @@ export function ArtifactFileDetail({
|
|||||||
const previewable = useMemo(() => {
|
const previewable = useMemo(() => {
|
||||||
return (language === "html" && !isWriteFile) || language === "markdown";
|
return (language === "html" && !isWriteFile) || language === "markdown";
|
||||||
}, [isWriteFile, language]);
|
}, [isWriteFile, language]);
|
||||||
const { thread } = useThread();
|
|
||||||
const { content } = useArtifactContent({
|
const { content } = useArtifactContent({
|
||||||
threadId,
|
threadId,
|
||||||
filepath: filepathFromProps,
|
filepath: filepathFromProps,
|
||||||
enabled: isCodeFile && !isWriteFile,
|
enabled: isCodeFile && !isWriteFile,
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = useParsedCitations(
|
const displayContent = content ?? "";
|
||||||
language === "markdown" ? (content ?? "") : "",
|
|
||||||
);
|
|
||||||
const cleanContent =
|
|
||||||
language === "markdown" && content ? parsed.cleanContent : (content ?? "");
|
|
||||||
const contentWithoutCitations =
|
|
||||||
language === "markdown" && content
|
|
||||||
? contentWithoutCitationsFromParsed(parsed)
|
|
||||||
: (content ?? "");
|
|
||||||
|
|
||||||
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
||||||
const [isInstalling, setIsInstalling] = useState(false);
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
@@ -219,7 +199,7 @@ export function ArtifactFileDetail({
|
|||||||
disabled={!content}
|
disabled={!content}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(contentWithoutCitations ?? "");
|
await navigator.clipboard.writeText(displayContent ?? "");
|
||||||
toast.success(t.clipboard.copiedToClipboard);
|
toast.success(t.clipboard.copiedToClipboard);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Failed to copy to clipboard");
|
toast.error("Failed to copy to clipboard");
|
||||||
@@ -255,27 +235,17 @@ export function ArtifactFileDetail({
|
|||||||
viewMode === "preview" &&
|
viewMode === "preview" &&
|
||||||
language === "markdown" &&
|
language === "markdown" &&
|
||||||
content && (
|
content && (
|
||||||
<SafeCitationContent
|
|
||||||
content={content}
|
|
||||||
isLoading={thread.isLoading}
|
|
||||||
rehypePlugins={streamdownPlugins.rehypePlugins}
|
|
||||||
className="flex size-full items-center justify-center p-4 my-0"
|
|
||||||
renderBody={(p) => (
|
|
||||||
<ArtifactFilePreview
|
<ArtifactFilePreview
|
||||||
filepath={filepath}
|
filepath={filepath}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
content={content}
|
content={displayContent}
|
||||||
language={language ?? "text"}
|
language={language ?? "text"}
|
||||||
cleanContent={p.cleanContent}
|
|
||||||
citationMap={p.citationMap}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isCodeFile && viewMode === "code" && (
|
{isCodeFile && viewMode === "code" && (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
className="size-full resize-none rounded-none border-none"
|
className="size-full resize-none rounded-none border-none"
|
||||||
value={cleanContent ?? ""}
|
value={displayContent ?? ""}
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -295,29 +265,17 @@ export function ArtifactFilePreview({
|
|||||||
threadId,
|
threadId,
|
||||||
content,
|
content,
|
||||||
language,
|
language,
|
||||||
cleanContent,
|
|
||||||
citationMap,
|
|
||||||
}: {
|
}: {
|
||||||
filepath: string;
|
filepath: string;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
content: string;
|
content: string;
|
||||||
language: string;
|
language: string;
|
||||||
cleanContent: string;
|
|
||||||
citationMap: Map<string, Citation>;
|
|
||||||
}) {
|
}) {
|
||||||
if (language === "markdown") {
|
if (language === "markdown") {
|
||||||
const components = createCitationMarkdownComponents({
|
|
||||||
citationMap,
|
|
||||||
syntheticExternal: true,
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full px-4">
|
<div className="size-full px-4">
|
||||||
<Streamdown
|
<Streamdown className="size-full" {...streamdownPlugins}>
|
||||||
className="size-full"
|
{content ?? ""}
|
||||||
{...streamdownPlugins}
|
|
||||||
components={components}
|
|
||||||
>
|
|
||||||
{cleanContent ?? ""}
|
|
||||||
</Streamdown>
|
</Streamdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ImgHTMLAttributes } from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MessageResponse,
|
||||||
|
type MessageResponseProps,
|
||||||
|
} from "@/components/ai-elements/message";
|
||||||
|
import { streamdownPlugins } from "@/core/streamdown";
|
||||||
|
|
||||||
|
export type MarkdownContentProps = {
|
||||||
|
content: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
rehypePlugins: MessageResponseProps["rehypePlugins"];
|
||||||
|
className?: string;
|
||||||
|
remarkPlugins?: MessageResponseProps["remarkPlugins"];
|
||||||
|
isHuman?: boolean;
|
||||||
|
img?: (props: ImgHTMLAttributes<HTMLImageElement> & { threadId?: string; maxWidth?: string }) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Renders markdown content. */
|
||||||
|
export function MarkdownContent({
|
||||||
|
content,
|
||||||
|
rehypePlugins,
|
||||||
|
className,
|
||||||
|
remarkPlugins = streamdownPlugins.remarkPlugins,
|
||||||
|
img,
|
||||||
|
}: MarkdownContentProps) {
|
||||||
|
if (!content) return null;
|
||||||
|
const components = img ? { img } : undefined;
|
||||||
|
return (
|
||||||
|
<MessageResponse
|
||||||
|
className={className}
|
||||||
|
remarkPlugins={remarkPlugins}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
components={components}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</MessageResponse>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -39,9 +39,7 @@ import { useArtifacts } from "../artifacts";
|
|||||||
import { FlipDisplay } from "../flip-display";
|
import { FlipDisplay } from "../flip-display";
|
||||||
import { Tooltip } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
|
|
||||||
import { useThread } from "./context";
|
import { MarkdownContent } from "./markdown-content";
|
||||||
|
|
||||||
import { SafeCitationContent } from "./safe-citation-content";
|
|
||||||
|
|
||||||
export function MessageGroup({
|
export function MessageGroup({
|
||||||
className,
|
className,
|
||||||
@@ -120,7 +118,7 @@ export function MessageGroup({
|
|||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
key={step.id}
|
key={step.id}
|
||||||
label={
|
label={
|
||||||
<SafeCitationContent
|
<MarkdownContent
|
||||||
content={step.reasoning ?? ""}
|
content={step.reasoning ?? ""}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
@@ -128,12 +126,7 @@ export function MessageGroup({
|
|||||||
}
|
}
|
||||||
></ChainOfThoughtStep>
|
></ChainOfThoughtStep>
|
||||||
) : (
|
) : (
|
||||||
<ToolCall
|
<ToolCall key={step.id} {...step} isLoading={isLoading} />
|
||||||
key={step.id}
|
|
||||||
{...step}
|
|
||||||
isLoading={isLoading}
|
|
||||||
rehypePlugins={rehypePlugins}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
{lastToolCallStep && (
|
{lastToolCallStep && (
|
||||||
@@ -143,7 +136,6 @@ export function MessageGroup({
|
|||||||
{...lastToolCallStep}
|
{...lastToolCallStep}
|
||||||
isLast={true}
|
isLast={true}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
rehypePlugins={rehypePlugins}
|
|
||||||
/>
|
/>
|
||||||
</FlipDisplay>
|
</FlipDisplay>
|
||||||
)}
|
)}
|
||||||
@@ -178,7 +170,7 @@ export function MessageGroup({
|
|||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
key={lastReasoningStep.id}
|
key={lastReasoningStep.id}
|
||||||
label={
|
label={
|
||||||
<SafeCitationContent
|
<MarkdownContent
|
||||||
content={lastReasoningStep.reasoning ?? ""}
|
content={lastReasoningStep.reasoning ?? ""}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
@@ -201,7 +193,6 @@ function ToolCall({
|
|||||||
result,
|
result,
|
||||||
isLast = false,
|
isLast = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
rehypePlugins,
|
|
||||||
}: {
|
}: {
|
||||||
id?: string;
|
id?: string;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
@@ -210,15 +201,10 @@ function ToolCall({
|
|||||||
result?: string | Record<string, unknown>;
|
result?: string | Record<string, unknown>;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
rehypePlugins: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { setOpen, autoOpen, autoSelect, selectedArtifact, select } =
|
const { setOpen, autoOpen, autoSelect, selectedArtifact, select } =
|
||||||
useArtifacts();
|
useArtifacts();
|
||||||
const { thread } = useThread();
|
|
||||||
const threadIsLoading = thread.isLoading;
|
|
||||||
|
|
||||||
const fileContent = typeof args.content === "string" ? args.content : "";
|
|
||||||
|
|
||||||
if (name === "web_search") {
|
if (name === "web_search") {
|
||||||
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
|
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
|
||||||
@@ -364,12 +350,7 @@ function ToolCall({
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMarkdown =
|
|
||||||
path?.toLowerCase().endsWith(".md") ||
|
|
||||||
path?.toLowerCase().endsWith(".markdown");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
key={id}
|
key={id}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
@@ -390,16 +371,6 @@ function ToolCall({
|
|||||||
</ChainOfThoughtSearchResult>
|
</ChainOfThoughtSearchResult>
|
||||||
)}
|
)}
|
||||||
</ChainOfThoughtStep>
|
</ChainOfThoughtStep>
|
||||||
{isMarkdown && (
|
|
||||||
<SafeCitationContent
|
|
||||||
content={fileContent}
|
|
||||||
isLoading={threadIsLoading && isLast}
|
|
||||||
rehypePlugins={rehypePlugins}
|
|
||||||
loadingOnly
|
|
||||||
className="mt-2 ml-8"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
} else if (name === "bash") {
|
} else if (name === "bash") {
|
||||||
const description: string | undefined = (args as { description: string })
|
const description: string | undefined = (args as { description: string })
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from "@/components/ai-elements/message";
|
} from "@/components/ai-elements/message";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { resolveArtifactURL } from "@/core/artifacts/utils";
|
import { resolveArtifactURL } from "@/core/artifacts/utils";
|
||||||
import { removeAllCitations } from "@/core/citations";
|
|
||||||
import {
|
import {
|
||||||
extractContentFromMessage,
|
extractContentFromMessage,
|
||||||
extractReasoningContentFromMessage,
|
extractReasoningContentFromMessage,
|
||||||
@@ -24,7 +23,7 @@ import { humanMessagePlugins } from "@/core/streamdown";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { CopyButton } from "../copy-button";
|
import { CopyButton } from "../copy-button";
|
||||||
import { SafeCitationContent } from "./safe-citation-content";
|
import { MarkdownContent } from "./markdown-content";
|
||||||
|
|
||||||
export function MessageListItem({
|
export function MessageListItem({
|
||||||
className,
|
className,
|
||||||
@@ -54,11 +53,11 @@ export function MessageListItem({
|
|||||||
>
|
>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<CopyButton
|
<CopyButton
|
||||||
clipboardData={removeAllCitations(
|
clipboardData={
|
||||||
extractContentFromMessage(message) ??
|
extractContentFromMessage(message) ??
|
||||||
extractReasoningContentFromMessage(message) ??
|
extractReasoningContentFromMessage(message) ??
|
||||||
""
|
""
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</MessageToolbar>
|
</MessageToolbar>
|
||||||
@@ -154,7 +153,7 @@ function MessageContent_({
|
|||||||
return (
|
return (
|
||||||
<AIElementMessageContent className={className}>
|
<AIElementMessageContent className={className}>
|
||||||
{filesList}
|
{filesList}
|
||||||
<SafeCitationContent
|
<MarkdownContent
|
||||||
content={contentToParse}
|
content={contentToParse}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: "html" }]]}
|
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: "html" }]]}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { StreamingIndicator } from "../streaming-indicator";
|
|||||||
|
|
||||||
import { MessageGroup } from "./message-group";
|
import { MessageGroup } from "./message-group";
|
||||||
import { MessageListItem } from "./message-list-item";
|
import { MessageListItem } from "./message-list-item";
|
||||||
import { SafeCitationContent } from "./safe-citation-content";
|
import { MarkdownContent } from "./markdown-content";
|
||||||
import { MessageListSkeleton } from "./skeleton";
|
import { MessageListSkeleton } from "./skeleton";
|
||||||
import { SubtaskCard } from "./subtask-card";
|
import { SubtaskCard } from "./subtask-card";
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export function MessageList({
|
|||||||
const message = group.messages[0];
|
const message = group.messages[0];
|
||||||
if (message && hasContent(message)) {
|
if (message && hasContent(message)) {
|
||||||
return (
|
return (
|
||||||
<SafeCitationContent
|
<MarkdownContent
|
||||||
key={group.id}
|
key={group.id}
|
||||||
content={extractContentFromMessage(message)}
|
content={extractContentFromMessage(message)}
|
||||||
isLoading={thread.isLoading}
|
isLoading={thread.isLoading}
|
||||||
@@ -89,7 +89,7 @@ export function MessageList({
|
|||||||
return (
|
return (
|
||||||
<div className="w-full" key={group.id}>
|
<div className="w-full" key={group.id}>
|
||||||
{group.messages[0] && hasContent(group.messages[0]) && (
|
{group.messages[0] && hasContent(group.messages[0]) && (
|
||||||
<SafeCitationContent
|
<MarkdownContent
|
||||||
content={extractContentFromMessage(group.messages[0])}
|
content={extractContentFromMessage(group.messages[0])}
|
||||||
isLoading={thread.isLoading}
|
isLoading={thread.isLoading}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { ImgHTMLAttributes } from "react";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
CitationsLoadingIndicator,
|
|
||||||
createCitationMarkdownComponents,
|
|
||||||
} from "@/components/ai-elements/inline-citation";
|
|
||||||
import {
|
|
||||||
MessageResponse,
|
|
||||||
type MessageResponseProps,
|
|
||||||
} from "@/components/ai-elements/message";
|
|
||||||
import {
|
|
||||||
shouldShowCitationLoading,
|
|
||||||
useParsedCitations,
|
|
||||||
type UseParsedCitationsResult,
|
|
||||||
} from "@/core/citations";
|
|
||||||
import { streamdownPlugins } from "@/core/streamdown";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type SafeCitationContentProps = {
|
|
||||||
content: string;
|
|
||||||
isLoading: boolean;
|
|
||||||
rehypePlugins: MessageResponseProps["rehypePlugins"];
|
|
||||||
className?: string;
|
|
||||||
remarkPlugins?: MessageResponseProps["remarkPlugins"];
|
|
||||||
isHuman?: boolean;
|
|
||||||
img?: (props: ImgHTMLAttributes<HTMLImageElement> & { threadId?: string; maxWidth?: string }) => ReactNode;
|
|
||||||
/** When true, only show loading indicator or null (e.g. write_file step). */
|
|
||||||
loadingOnly?: boolean;
|
|
||||||
/** When set, use instead of default MessageResponse (e.g. artifact preview). */
|
|
||||||
renderBody?: (parsed: UseParsedCitationsResult) => ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Single place for citation-aware body: shows loading until citations complete (no half-finished refs), else body. */
|
|
||||||
export function SafeCitationContent({
|
|
||||||
content,
|
|
||||||
isLoading,
|
|
||||||
rehypePlugins,
|
|
||||||
className,
|
|
||||||
remarkPlugins = streamdownPlugins.remarkPlugins,
|
|
||||||
isHuman = false,
|
|
||||||
img,
|
|
||||||
loadingOnly = false,
|
|
||||||
renderBody,
|
|
||||||
}: SafeCitationContentProps) {
|
|
||||||
const parsed = useParsedCitations(content);
|
|
||||||
const { citations, cleanContent, citationMap } = parsed;
|
|
||||||
const showLoading = shouldShowCitationLoading(content, cleanContent, isLoading);
|
|
||||||
const components = useMemo(
|
|
||||||
() =>
|
|
||||||
createCitationMarkdownComponents({
|
|
||||||
citationMap,
|
|
||||||
isHuman,
|
|
||||||
isLoadingCitations: false,
|
|
||||||
img,
|
|
||||||
}),
|
|
||||||
[citationMap, isHuman, img],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (showLoading) {
|
|
||||||
return (
|
|
||||||
<CitationsLoadingIndicator
|
|
||||||
citations={citations}
|
|
||||||
className={cn("my-2", className)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (loadingOnly) return null;
|
|
||||||
if (renderBody) return renderBody(parsed);
|
|
||||||
if (!cleanContent) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MessageResponse
|
|
||||||
className={className}
|
|
||||||
remarkPlugins={remarkPlugins}
|
|
||||||
rehypePlugins={rehypePlugins}
|
|
||||||
components={components}
|
|
||||||
>
|
|
||||||
{cleanContent}
|
|
||||||
</MessageResponse>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,7 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
import { FlipDisplay } from "../flip-display";
|
import { FlipDisplay } from "../flip-display";
|
||||||
|
|
||||||
import { SafeCitationContent } from "./safe-citation-content";
|
import { MarkdownContent } from "./markdown-content";
|
||||||
|
|
||||||
export function SubtaskCard({
|
export function SubtaskCard({
|
||||||
className,
|
className,
|
||||||
@@ -153,7 +153,7 @@ export function SubtaskCard({
|
|||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
label={
|
label={
|
||||||
task.result ? (
|
task.result ? (
|
||||||
<SafeCitationContent
|
<MarkdownContent
|
||||||
content={task.result}
|
content={task.result}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
export {
|
|
||||||
contentWithoutCitationsFromParsed,
|
|
||||||
extractDomainFromUrl,
|
|
||||||
isExternalUrl,
|
|
||||||
parseCitations,
|
|
||||||
removeAllCitations,
|
|
||||||
shouldShowCitationLoading,
|
|
||||||
syntheticCitationFromLink,
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
export { useParsedCitations } from "./use-parsed-citations";
|
|
||||||
export type { UseParsedCitationsResult } from "./use-parsed-citations";
|
|
||||||
export type { Citation, ParseCitationsResult } from "./utils";
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
import { parseCitations } from "./utils";
|
|
||||||
import type { Citation } from "./utils";
|
|
||||||
|
|
||||||
export interface UseParsedCitationsResult {
|
|
||||||
citations: Citation[];
|
|
||||||
cleanContent: string;
|
|
||||||
citationMap: Map<string, Citation>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse content for citations and build citation map. Memoized by content.
|
|
||||||
*/
|
|
||||||
export function useParsedCitations(content: string): UseParsedCitationsResult {
|
|
||||||
return useMemo(() => {
|
|
||||||
const parsed = parseCitations(content ?? "");
|
|
||||||
const citationMap = new Map<string, Citation>();
|
|
||||||
for (const c of parsed.citations) citationMap.set(c.url, c);
|
|
||||||
return {
|
|
||||||
citations: parsed.citations,
|
|
||||||
cleanContent: parsed.cleanContent,
|
|
||||||
citationMap,
|
|
||||||
};
|
|
||||||
}, [content]);
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
/**
|
|
||||||
* Citation parsing and display helpers.
|
|
||||||
* Display rule: never show half-finished citations. Use shouldShowCitationLoading
|
|
||||||
* and show only the loading indicator until the block is complete and all
|
|
||||||
* [cite-N] refs are replaced.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Citation data structure representing a source reference
|
|
||||||
*/
|
|
||||||
export interface Citation {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
snippet: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result of parsing citations from content
|
|
||||||
*/
|
|
||||||
export interface ParseCitationsResult {
|
|
||||||
citations: Citation[];
|
|
||||||
cleanContent: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse citation lines (one JSON object per line) into Citation array.
|
|
||||||
* Deduplicates by URL. Used for both complete and incomplete (streaming) blocks.
|
|
||||||
*/
|
|
||||||
function parseCitationLines(
|
|
||||||
blockContent: string,
|
|
||||||
seenUrls: Set<string>,
|
|
||||||
): Citation[] {
|
|
||||||
const out: Citation[] = [];
|
|
||||||
const lines = blockContent.split("\n");
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (!trimmed?.startsWith("{")) continue;
|
|
||||||
try {
|
|
||||||
const citation = JSON.parse(trimmed) as Citation;
|
|
||||||
if (citation.id && citation.url && !seenUrls.has(citation.url)) {
|
|
||||||
seenUrls.add(citation.url);
|
|
||||||
out.push({
|
|
||||||
id: citation.id,
|
|
||||||
title: citation.title || "",
|
|
||||||
url: citation.url,
|
|
||||||
snippet: citation.snippet || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Skip invalid JSON lines - can happen during streaming
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse citations block from message content.
|
|
||||||
* Shared by all modes (Flash / Thinking / Pro / Ultra); supports incomplete
|
|
||||||
* <citations> blocks during SSE streaming (parses whatever complete JSON lines
|
|
||||||
* have arrived so far so [cite-N] can be linked progressively).
|
|
||||||
*
|
|
||||||
* The citations block format:
|
|
||||||
* <citations>
|
|
||||||
* {"id": "cite-1", "title": "Page Title", "url": "https://example.com", "snippet": "Description"}
|
|
||||||
* {"id": "cite-2", "title": "Another Page", "url": "https://example2.com", "snippet": "Description"}
|
|
||||||
* </citations>
|
|
||||||
*
|
|
||||||
* @param content - The raw message content that may contain a citations block
|
|
||||||
* @returns Object containing parsed citations array and content with citations block removed
|
|
||||||
*/
|
|
||||||
export function parseCitations(content: string): ParseCitationsResult {
|
|
||||||
if (!content) {
|
|
||||||
return { citations: [], cleanContent: content };
|
|
||||||
}
|
|
||||||
|
|
||||||
const citations: Citation[] = [];
|
|
||||||
const seenUrls = new Set<string>();
|
|
||||||
|
|
||||||
// 1) Complete blocks: <citations>...</citations>
|
|
||||||
const citationsRegex = /<citations>([\s\S]*?)<\/citations>/g;
|
|
||||||
let match;
|
|
||||||
while ((match = citationsRegex.exec(content)) !== null) {
|
|
||||||
citations.push(...parseCitationLines(match[1] ?? "", seenUrls));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Incomplete block during streaming: <citations>... (no closing tag yet)
|
|
||||||
if (content.includes("<citations>") && !content.includes("</citations>")) {
|
|
||||||
const openMatch = content.match(/<citations>([\s\S]*)$/);
|
|
||||||
if (openMatch?.[1] != null) {
|
|
||||||
citations.push(...parseCitationLines(openMatch[1], seenUrls));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let cleanContent = removeCitationsBlocks(content);
|
|
||||||
|
|
||||||
// Convert [cite-N] references to markdown links
|
|
||||||
// Example: [cite-1] -> [Title](url)
|
|
||||||
if (citations.length > 0) {
|
|
||||||
// Build a map from citation id to citation object
|
|
||||||
const idMap = new Map<string, Citation>();
|
|
||||||
for (const citation of citations) {
|
|
||||||
idMap.set(citation.id, citation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace all [cite-N] patterns with markdown links
|
|
||||||
cleanContent = cleanContent.replace(/\[cite-(\d+)\]/g, (match, num) => {
|
|
||||||
const citeId = `cite-${num}`;
|
|
||||||
const citation = idMap.get(citeId);
|
|
||||||
if (citation) {
|
|
||||||
// Use title if available, otherwise use domain
|
|
||||||
const linkText = citation.title || extractDomainFromUrl(citation.url);
|
|
||||||
return `[${linkText}](${citation.url})`;
|
|
||||||
}
|
|
||||||
// If citation not found, keep the original text
|
|
||||||
return match;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { citations, cleanContent };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the URL is external (http/https).
|
|
||||||
*/
|
|
||||||
export function isExternalUrl(url: string): boolean {
|
|
||||||
return url.startsWith("http://") || url.startsWith("https://");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a synthetic Citation from a link (e.g. in artifact markdown without <citations> block).
|
|
||||||
*/
|
|
||||||
export function syntheticCitationFromLink(href: string, title: string): Citation {
|
|
||||||
return {
|
|
||||||
id: `artifact-cite-${href}`,
|
|
||||||
title: title || href,
|
|
||||||
url: href,
|
|
||||||
snippet: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract the domain name from a URL for display
|
|
||||||
*
|
|
||||||
* @param url - Full URL string
|
|
||||||
* @returns Domain name or the original URL if parsing fails
|
|
||||||
*/
|
|
||||||
export function extractDomainFromUrl(url: string): string {
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(url);
|
|
||||||
// Remove 'www.' prefix if present
|
|
||||||
return urlObj.hostname.replace(/^www\./, "");
|
|
||||||
} catch {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all <citations> blocks from content (complete and incomplete).
|
|
||||||
* Does not remove [cite-N] or markdown links; use removeAllCitations for that.
|
|
||||||
*/
|
|
||||||
export function removeCitationsBlocks(content: string): string {
|
|
||||||
if (!content) return content;
|
|
||||||
let result = content.replace(/<citations>[\s\S]*?<\/citations>/g, "").trim();
|
|
||||||
if (result.includes("<citations>")) {
|
|
||||||
result = result.replace(/<citations>[\s\S]*$/g, "").trim();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether content contains a <citations> block (open tag).
|
|
||||||
*/
|
|
||||||
export function hasCitationsBlock(content: string): boolean {
|
|
||||||
return Boolean(content?.includes("<citations>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Pattern for [cite-1], [cite-2], ... that should be replaced by parseCitations. */
|
|
||||||
const UNREPLACED_CITE_REF = /\[cite-\d+\]/;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether cleanContent still contains unreplaced [cite-N] refs (half-finished citations).
|
|
||||||
* When true, callers must not render this content and should show loading instead.
|
|
||||||
*/
|
|
||||||
export function hasUnreplacedCitationRefs(cleanContent: string): boolean {
|
|
||||||
return Boolean(cleanContent && UNREPLACED_CITE_REF.test(cleanContent));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Single source of truth: true when body must not be rendered (show loading instead).
|
|
||||||
* Use after parseCitations: pass raw content, parsed cleanContent, and isLoading.
|
|
||||||
* Never show body when cleanContent still has [cite-N] (e.g. refs arrived before
|
|
||||||
* <citations> block in stream); also show loading while streaming with citation block.
|
|
||||||
*/
|
|
||||||
export function shouldShowCitationLoading(
|
|
||||||
rawContent: string,
|
|
||||||
cleanContent: string,
|
|
||||||
isLoading: boolean,
|
|
||||||
): boolean {
|
|
||||||
if (hasUnreplacedCitationRefs(cleanContent)) return true;
|
|
||||||
return isLoading && hasCitationsBlock(rawContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strip citation markdown links from already-cleaned content (from parseCitations).
|
|
||||||
* Use when you already have ParseCitationsResult to avoid parsing twice.
|
|
||||||
*/
|
|
||||||
export function contentWithoutCitationsFromParsed(
|
|
||||||
parsed: ParseCitationsResult,
|
|
||||||
): string {
|
|
||||||
const citationUrls = new Set(parsed.citations.map((c) => c.url));
|
|
||||||
const withoutLinks = parsed.cleanContent.replace(
|
|
||||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
||||||
(fullMatch, _text, url) => (citationUrls.has(url) ? "" : fullMatch),
|
|
||||||
);
|
|
||||||
return withoutLinks.replace(/\n{3,}/g, "\n\n").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove ALL citations from content (blocks, [cite-N], and citation links).
|
|
||||||
* Used for copy/download. For display you typically use parseCitations/useParsedCitations.
|
|
||||||
*/
|
|
||||||
export function removeAllCitations(content: string): string {
|
|
||||||
if (!content) return content;
|
|
||||||
return contentWithoutCitationsFromParsed(parseCitations(content));
|
|
||||||
}
|
|
||||||
@@ -167,13 +167,6 @@ export const enUS: Translations = {
|
|||||||
startConversation: "Start a conversation to see messages here",
|
startConversation: "Start a conversation to see messages here",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Citations
|
|
||||||
citations: {
|
|
||||||
loadingCitations: "Organizing citations...",
|
|
||||||
loadingCitationsWithCount: (count: number) =>
|
|
||||||
`Organizing ${count} citation${count === 1 ? "" : "s"}...`,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Chats
|
// Chats
|
||||||
chats: {
|
chats: {
|
||||||
searchChats: "Search chats",
|
searchChats: "Search chats",
|
||||||
|
|||||||
@@ -115,12 +115,6 @@ export interface Translations {
|
|||||||
startConversation: string;
|
startConversation: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Citations
|
|
||||||
citations: {
|
|
||||||
loadingCitations: string;
|
|
||||||
loadingCitationsWithCount: (count: number) => string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Chats
|
// Chats
|
||||||
chats: {
|
chats: {
|
||||||
searchChats: string;
|
searchChats: string;
|
||||||
|
|||||||
@@ -164,12 +164,6 @@ export const zhCN: Translations = {
|
|||||||
startConversation: "开始新的对话以查看消息",
|
startConversation: "开始新的对话以查看消息",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Citations
|
|
||||||
citations: {
|
|
||||||
loadingCitations: "正在整理引用...",
|
|
||||||
loadingCitationsWithCount: (count: number) => `正在整理 ${count} 个引用...`,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Chats
|
// Chats
|
||||||
chats: {
|
chats: {
|
||||||
searchChats: "搜索对话",
|
searchChats: "搜索对话",
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
/** Shared class for external links (underline by default). */
|
/** Shared class for external links (underline by default). */
|
||||||
export const externalLinkClass =
|
export const externalLinkClass =
|
||||||
"text-primary underline underline-offset-2 hover:no-underline";
|
"text-primary underline underline-offset-2 hover:no-underline";
|
||||||
/** For streaming / loading state when link may be a citation (no underline). */
|
/** Link style without underline by default (e.g. for streaming/loading). */
|
||||||
export const externalLinkClassNoUnderline = "text-primary hover:underline";
|
export const externalLinkClassNoUnderline = "text-primary hover:underline";
|
||||||
|
|||||||
@@ -147,5 +147,5 @@ Save report as: `research_{topic}_{YYYYMMDD}.md`
|
|||||||
3. **Triangulate claims** - 2+ independent sources
|
3. **Triangulate claims** - 2+ independent sources
|
||||||
4. **Note conflicting info** - Don't hide contradictions
|
4. **Note conflicting info** - Don't hide contradictions
|
||||||
5. **Distinguish fact vs opinion** - Label speculation clearly
|
5. **Distinguish fact vs opinion** - Label speculation clearly
|
||||||
6. **Cite inline** - Reference sources near claims
|
6. **Reference sources** - Add source references near claims where applicable
|
||||||
7. **Update as you go** - Don't wait until end to synthesize
|
7. **Update as you go** - Don't wait until end to synthesize
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ This skill generates professional, consulting-grade market analysis reports in M
|
|||||||
- Follow the **"Visual Anchor → Data Contrast → Integrated Analysis"** flow per sub-chapter
|
- Follow the **"Visual Anchor → Data Contrast → Integrated Analysis"** flow per sub-chapter
|
||||||
- Produce insights following the **"Data → User Psychology → Strategy Implication"** chain
|
- Produce insights following the **"Data → User Psychology → Strategy Implication"** chain
|
||||||
- Embed pre-generated charts and construct comparison tables
|
- Embed pre-generated charts and construct comparison tables
|
||||||
- Generate inline citations formatted per **GB/T 7714-2015** standards
|
- Include references formatted per **GB/T 7714-2015** where applicable
|
||||||
- Output reports entirely in Chinese with professional consulting tone
|
- Output reports entirely in Chinese with professional consulting tone
|
||||||
|
|
||||||
## When to Use This Skill
|
## When to Use This Skill
|
||||||
@@ -36,7 +36,7 @@ The skill expects the following inputs from the upstream agentic workflow:
|
|||||||
| **Analysis Framework Outline** | Defines the logic flow and general topics for the report | Yes |
|
| **Analysis Framework Outline** | Defines the logic flow and general topics for the report | Yes |
|
||||||
| **Data Summary** | The source of truth containing raw numbers and metrics | Yes |
|
| **Data Summary** | The source of truth containing raw numbers and metrics | Yes |
|
||||||
| **Chart Files** | Local file paths for pre-generated chart images | Yes |
|
| **Chart Files** | Local file paths for pre-generated chart images | Yes |
|
||||||
| **External Search Findings** | URLs and summaries for inline citations | Optional |
|
| **External Search Findings** | URLs and summaries for inline references | Optional |
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ The report **MUST NOT** stop after the Conclusion — it **MUST** include Refere
|
|||||||
- **Tone**: McKinsey/BCG — Authoritative, Objective, Professional
|
- **Tone**: McKinsey/BCG — Authoritative, Objective, Professional
|
||||||
- **Language**: All headings and content strictly in **Chinese**
|
- **Language**: All headings and content strictly in **Chinese**
|
||||||
- **Number Formatting**: Use English commas for thousands separators (`1,000` not `1,000`)
|
- **Number Formatting**: Use English commas for thousands separators (`1,000` not `1,000`)
|
||||||
- **Data Citation**: **Bold** important viewpoints and key numbers
|
- **Data emphasis**: **Bold** important viewpoints and key numbers
|
||||||
|
|
||||||
### Titling Constraints
|
### Titling Constraints
|
||||||
- **Numbering**: Use standard numbering (`1.`, `1.1`) or Chinese numbering (`一、`) directly followed by the title
|
- **Numbering**: Use standard numbering (`1.`, `1.1`) or Chinese numbering (`一、`) directly followed by the title
|
||||||
@@ -109,11 +109,9 @@ Every insight must connect **Data → User Psychology → Strategy Implication**
|
|||||||
treating male audiences only as a secondary gift-giving segment."
|
treating male audiences only as a secondary gift-giving segment."
|
||||||
```
|
```
|
||||||
|
|
||||||
### Citations & References
|
### References
|
||||||
- **Inline**: Use `[\[Index\]](URL)` format (e.g., `[\[1\]](https://example.com)`)
|
- **Inline**: Use markdown links for sources (e.g. `[Source Title](URL)`) when using External Search Findings
|
||||||
- **Placement**: Append citations at the end of sentences using information from External Search Findings
|
- **References section**: Formatted strictly per **GB/T 7714-2015**
|
||||||
- **Index Assignment**: Sequential starting from **1** based on order of appearance
|
|
||||||
- **References Section**: Formatted strictly per **GB/T 7714-2015**
|
|
||||||
|
|
||||||
### Markdown Rules
|
### Markdown Rules
|
||||||
- **Immediate Start**: Begin directly with `# Report Title` — no introductory text
|
- **Immediate Start**: Begin directly with `# Report Title` — no introductory text
|
||||||
@@ -183,7 +181,7 @@ Before considering the report complete, verify:
|
|||||||
- [ ] All headings are in Chinese with proper numbering (no "Chapter/Part/Section")
|
- [ ] All headings are in Chinese with proper numbering (no "Chapter/Part/Section")
|
||||||
- [ ] Charts are embedded with `` syntax
|
- [ ] Charts are embedded with `` syntax
|
||||||
- [ ] Numbers use English commas for thousands separators
|
- [ ] Numbers use English commas for thousands separators
|
||||||
- [ ] Inline citations use `[\[N\]](URL)` format
|
- [ ] Inline references use markdown links where applicable
|
||||||
- [ ] References section follows GB/T 7714-2015
|
- [ ] References section follows GB/T 7714-2015
|
||||||
- [ ] No horizontal rules (`---`) in the document
|
- [ ] No horizontal rules (`---`) in the document
|
||||||
- [ ] Conclusion uses flowing prose — no bullet points
|
- [ ] Conclusion uses flowing prose — no bullet points
|
||||||
|
|||||||
Reference in New Issue
Block a user