feat: refine citations format and improve content presentation

Backend:
- Simplify citations prompt format and rules
- Add clear distinction between chat responses and file content
- Enforce full URL usage in markdown links, prohibit [cite-1] format
- Require content-first approach: write full content, then add citations at end

Frontend:
- Hide <citations> block in both chat messages and markdown preview
- Remove top-level Citations/Sources list for cleaner UI
- Auto-remove <citations> block in code editor view for markdown files
- Keep inline citation hover cards for reference details

This ensures citations are presented like Claude: clean content with inline reference badges.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ruitanglin
2026-01-29 12:29:13 +08:00
parent 33c47e0c56
commit 4b63e70b7e
10 changed files with 515 additions and 185 deletions

View File

@@ -123,31 +123,33 @@ You have access to skills that provide optimized workflows for specific tasks. E
</response_style> </response_style>
<citations_format> <citations_format>
**AUTOMATIC CITATION REQUIREMENT**: After using web_search tool, you MUST include citations in your response. **FORMAT** - After web_search, ALWAYS include citations in your output:
**For chat responses:**
**FORMAT** - Your response MUST start with a citations block, then content with inline links: Your visible response MUST start with citations block, then content with inline links:
<citations> <citations>
{{"id": "cite-1", "title": "Page Title", "url": "https://example.com/page", "snippet": "Brief description"}} {{"id": "cite-1", "title": "Page Title", "url": "https://example.com/page", "snippet": "Brief description"}}
{{"id": "cite-2", "title": "Another Source", "url": "https://another.com/article", "snippet": "What this covers"}}
</citations> </citations>
Content with inline links...
Then your content: According to [Source Name](url), the findings show... [Another Source](url2) also reports... **For files (write_file):**
File content MUST start with citations block, then content with inline links:
<citations>
{{"id": "cite-1", "title": "Page Title", "url": "https://example.com/page", "snippet": "Brief description"}}
</citations>
# Document Title
Content with inline [Source Name](full_url) links...
**RULES:** **RULES:**
- DO NOT put citations in your thinking/reasoning - output them in your VISIBLE RESPONSE - `<citations>` block MUST be FIRST (in both chat response AND file content)
- DO NOT wait for user to ask - output citations AUTOMATICALLY after web search - Write full content naturally, add [Source Name](full_url) at end of sentence/paragraph
- DO NOT use number format like [1] or [2] - use source name like [Reuters](url) - NEVER use "According to [Source]" format - write content first, then add citation link at end
- The `<citations>` block MUST be FIRST in your response (before any other text) - Example: "AI agents will transform digital work ([Microsoft](url))" NOT "According to [Microsoft](url), AI agents will..."
- Use source domain/brand name as link text (e.g., "Reuters", "TechCrunch", "智源研究院")
- The URL in markdown link must match a URL in your citations block
**IF writing markdown files**: When user asks you to create a report/document and you use write_file, use `[Source Name](url)` links in the file content (no <citations> block needed in files).
**Example:** **Example:**
<citations> <citations>
{{"id": "cite-1", "title": "AI Trends 2026", "url": "https://techcrunch.com/ai-trends", "snippet": "Tech industry predictions"}} {{"id": "cite-1", "title": "AI Trends 2026", "url": "https://techcrunch.com/ai-trends", "snippet": "Tech industry predictions"}}
</citations> </citations>
Based on [TechCrunch](https://techcrunch.com/ai-trends), the key AI trends for 2026 include... The key AI trends for 2026 include enhanced reasoning capabilities, multimodal integration, and improved efficiency [TechCrunch](https://techcrunch.com/ai-trends).
</citations_format> </citations_format>
<critical_reminders> <critical_reminders>

View File

@@ -1,6 +1,7 @@
"""Middleware to inject uploaded files information into agent context.""" """Middleware to inject uploaded files information into agent context."""
import os import os
import re
from pathlib import Path from pathlib import Path
from typing import NotRequired, override from typing import NotRequired, override
@@ -47,14 +48,15 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
""" """
return Path(self._base_dir) / THREAD_DATA_BASE_DIR / thread_id / "user-data" / "uploads" return Path(self._base_dir) / THREAD_DATA_BASE_DIR / thread_id / "user-data" / "uploads"
def _list_uploaded_files(self, thread_id: str) -> list[dict]: def _list_newly_uploaded_files(self, thread_id: str, last_message_files: set[str]) -> list[dict]:
"""List all files in the uploads directory. """List only newly uploaded files that weren't in the last message.
Args: Args:
thread_id: The thread ID. thread_id: The thread ID.
last_message_files: Set of filenames that were already shown in previous messages.
Returns: Returns:
List of file information dictionaries. List of new file information dictionaries.
""" """
uploads_dir = self._get_uploads_dir(thread_id) uploads_dir = self._get_uploads_dir(thread_id)
@@ -63,7 +65,7 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
files = [] files = []
for file_path in sorted(uploads_dir.iterdir()): for file_path in sorted(uploads_dir.iterdir()):
if file_path.is_file(): if file_path.is_file() and file_path.name not in last_message_files:
stat = file_path.stat() stat = file_path.stat()
files.append( files.append(
{ {
@@ -106,10 +108,41 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
return "\n".join(lines) return "\n".join(lines)
def _extract_files_from_message(self, content: str) -> set[str]:
"""Extract filenames from uploaded_files tag in message content.
Args:
content: Message content that may contain <uploaded_files> tag.
Returns:
Set of filenames mentioned in the tag.
"""
# Match <uploaded_files>...</uploaded_files> tag
match = re.search(r"<uploaded_files>([\s\S]*?)</uploaded_files>", content)
if not match:
return set()
files_content = match.group(1)
# Extract filenames from lines like "- filename.ext (size)"
# Need to capture everything before the opening parenthesis, including spaces
filenames = set()
for line in files_content.split("\n"):
# Match pattern: - filename with spaces.ext (size)
# Changed from [^\s(]+ to [^(]+ to allow spaces in filename
file_match = re.match(r"^-\s+(.+?)\s*\(", line.strip())
if file_match:
filenames.add(file_match.group(1).strip())
return filenames
@override @override
def before_agent(self, state: UploadsMiddlewareState, runtime: Runtime) -> dict | None: def before_agent(self, state: UploadsMiddlewareState, runtime: Runtime) -> dict | None:
"""Inject uploaded files information before agent execution. """Inject uploaded files information before agent execution.
Only injects files that weren't already shown in previous messages.
Prepends file info to the last human message content.
Args: Args:
state: Current agent state. state: Current agent state.
runtime: Runtime context containing thread_id. runtime: Runtime context containing thread_id.
@@ -117,26 +150,56 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
Returns: Returns:
State updates including uploaded files list. State updates including uploaded files list.
""" """
import logging
logger = logging.getLogger(__name__)
thread_id = runtime.context.get("thread_id") thread_id = runtime.context.get("thread_id")
if thread_id is None: if thread_id is None:
return None return None
# List uploaded files messages = list(state.get("messages", []))
files = self._list_uploaded_files(thread_id) if not messages:
return None
# Track all filenames that have been shown in previous messages (EXCEPT the last one)
shown_files: set[str] = set()
for msg in messages[:-1]: # Scan all messages except the last one
if isinstance(msg, HumanMessage):
content = msg.content if isinstance(msg.content, str) else ""
extracted = self._extract_files_from_message(content)
shown_files.update(extracted)
if extracted:
logger.info(f"Found previously shown files: {extracted}")
logger.info(f"Total shown files from history: {shown_files}")
# List only newly uploaded files
files = self._list_newly_uploaded_files(thread_id, shown_files)
logger.info(f"Newly uploaded files to inject: {[f['filename'] for f in files]}")
if not files: if not files:
return None return None
# Create system message with file list # Find the last human message and prepend file info to it
last_message_index = len(messages) - 1
last_message = messages[last_message_index]
if not isinstance(last_message, HumanMessage):
return None
# Create files message and prepend to the last human message content
files_message = self._create_files_message(files) files_message = self._create_files_message(files)
files_human_message = HumanMessage(content=files_message) original_content = last_message.content if isinstance(last_message.content, str) else ""
# Create new message with combined content
updated_message = HumanMessage(
content=f"{files_message}\n\n{original_content}",
id=last_message.id,
additional_kwargs=last_message.additional_kwargs,
)
# Inject the message into the message history # Replace the last message
# This will be added before user messages messages[last_message_index] = updated_message
messages = list(state.get("messages", []))
insert_index = 0
messages.insert(insert_index, files_human_message)
return { return {
"uploaded_files": files, "uploaded_files": files,

View File

@@ -1,6 +1,7 @@
import mimetypes import mimetypes
import os import os
from pathlib import Path from pathlib import Path
from urllib.parse import quote
from fastapi import APIRouter, HTTPException, Request, Response from fastapi import APIRouter, HTTPException, Request, Response
from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse
@@ -104,9 +105,12 @@ async def get_artifact(thread_id: str, path: str, request: Request) -> FileRespo
mime_type, _ = mimetypes.guess_type(actual_path) mime_type, _ = mimetypes.guess_type(actual_path)
# Encode filename for Content-Disposition header (RFC 5987)
encoded_filename = quote(actual_path.name)
# 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"):
return FileResponse(path=actual_path, filename=actual_path.name, media_type=mime_type, headers={"Content-Disposition": f'attachment; filename="{actual_path.name}"'}) 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":
return HTMLResponse(content=actual_path.read_text()) return HTMLResponse(content=actual_path.read_text())
@@ -117,4 +121,4 @@ async def get_artifact(thread_id: str, path: str, request: Request) -> FileRespo
if is_text_file_by_content(actual_path): if is_text_file_by_content(actual_path):
return PlainTextResponse(content=actual_path.read_text(), media_type=mime_type) return PlainTextResponse(content=actual_path.read_text(), media_type=mime_type)
return Response(content=actual_path.read_bytes(), media_type=mime_type, headers={"Content-Disposition": f'inline; filename="{actual_path.name}"'}) return Response(content=actual_path.read_bytes(), media_type=mime_type, headers={"Content-Disposition": f"inline; filename*=UTF-8''{encoded_filename}"})

View File

@@ -61,6 +61,7 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"gsap": "^3.13.0", "gsap": "^3.13.0",
"hast": "^1.0.0", "hast": "^1.0.0",
"katex": "^0.16.28",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"motion": "^12.26.2", "motion": "^12.26.2",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
@@ -71,6 +72,8 @@
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-resizable-panels": "^4.4.1", "react-resizable-panels": "^4.4.1",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0",
"shiki": "3.15.0", "shiki": "3.15.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"streamdown": "1.4.0", "streamdown": "1.4.0",

View File

@@ -140,6 +140,9 @@ importers:
hast: hast:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
katex:
specifier: ^0.16.28
version: 0.16.28
lucide-react: lucide-react:
specifier: ^0.562.0 specifier: ^0.562.0
version: 0.562.0(react@19.2.3) version: 0.562.0(react@19.2.3)
@@ -170,6 +173,12 @@ importers:
react-resizable-panels: react-resizable-panels:
specifier: ^4.4.1 specifier: ^4.4.1
version: 4.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 4.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
rehype-katex:
specifier: ^7.0.1
version: 7.0.1
remark-math:
specifier: ^6.0.0
version: 6.0.0
shiki: shiki:
specifier: 3.15.0 specifier: 3.15.0
version: 3.15.0 version: 3.15.0
@@ -695,105 +704,89 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4': '@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4': '@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4': '@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4': '@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4': '@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4': '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4': '@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5': '@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5': '@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5': '@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5': '@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5': '@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5': '@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5': '@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5': '@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5': '@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -935,28 +928,24 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@16.1.4': '@next/swc-linux-arm64-musl@16.1.4':
resolution: {integrity: sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q==} resolution: {integrity: sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@16.1.4': '@next/swc-linux-x64-gnu@16.1.4':
resolution: {integrity: sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ==} resolution: {integrity: sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@16.1.4': '@next/swc-linux-x64-musl@16.1.4':
resolution: {integrity: sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw==} resolution: {integrity: sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@16.1.4': '@next/swc-win32-arm64-msvc@16.1.4':
resolution: {integrity: sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw==} resolution: {integrity: sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw==}
@@ -1537,28 +1526,24 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@resvg/resvg-js-linux-arm64-musl@2.6.2': '@resvg/resvg-js-linux-arm64-musl@2.6.2':
resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@resvg/resvg-js-linux-x64-gnu@2.6.2': '@resvg/resvg-js-linux-x64-gnu@2.6.2':
resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@resvg/resvg-js-linux-x64-musl@2.6.2': '@resvg/resvg-js-linux-x64-musl@2.6.2':
resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@resvg/resvg-js-win32-arm64-msvc@2.6.2': '@resvg/resvg-js-win32-arm64-msvc@2.6.2':
resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==}
@@ -1620,79 +1605,66 @@ packages:
resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.55.1': '@rollup/rollup-linux-arm-musleabihf@4.55.1':
resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.55.1': '@rollup/rollup-linux-arm64-gnu@4.55.1':
resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.55.1': '@rollup/rollup-linux-arm64-musl@4.55.1':
resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.55.1': '@rollup/rollup-linux-loong64-gnu@4.55.1':
resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.55.1': '@rollup/rollup-linux-loong64-musl@4.55.1':
resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.55.1': '@rollup/rollup-linux-ppc64-gnu@4.55.1':
resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.55.1': '@rollup/rollup-linux-ppc64-musl@4.55.1':
resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.55.1': '@rollup/rollup-linux-riscv64-gnu@4.55.1':
resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.55.1': '@rollup/rollup-linux-riscv64-musl@4.55.1':
resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.55.1': '@rollup/rollup-linux-s390x-gnu@4.55.1':
resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.55.1': '@rollup/rollup-linux-x64-gnu@4.55.1':
resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.55.1': '@rollup/rollup-linux-x64-musl@4.55.1':
resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.55.1': '@rollup/rollup-openbsd-x64@4.55.1':
resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==}
@@ -1835,28 +1807,24 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18': '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18': '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.18': '@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.18': '@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
@@ -2219,49 +2187,41 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1': '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1': '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1': '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1': '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1': '@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1': '@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -3643,8 +3603,8 @@ packages:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
katex@0.16.27: katex@0.16.28:
resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} resolution: {integrity: sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==}
hasBin: true hasBin: true
keyv@4.5.4: keyv@4.5.4:
@@ -3740,28 +3700,24 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2: lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.2: lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.2: lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2: lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
@@ -8817,7 +8773,7 @@ snapshots:
object.assign: 4.1.7 object.assign: 4.1.7
object.values: 1.2.1 object.values: 1.2.1
katex@0.16.27: katex@0.16.28:
dependencies: dependencies:
commander: 8.3.0 commander: 8.3.0
@@ -9149,7 +9105,7 @@ snapshots:
dagre-d3-es: 7.0.13 dagre-d3-es: 7.0.13
dayjs: 1.11.19 dayjs: 1.11.19
dompurify: 3.3.1 dompurify: 3.3.1
katex: 0.16.27 katex: 0.16.28
khroma: 2.1.0 khroma: 2.1.0
lodash-es: 4.17.22 lodash-es: 4.17.22
marked: 16.4.2 marked: 16.4.2
@@ -9239,7 +9195,7 @@ snapshots:
dependencies: dependencies:
'@types/katex': 0.16.8 '@types/katex': 0.16.8
devlop: 1.1.0 devlop: 1.1.0
katex: 0.16.27 katex: 0.16.28
micromark-factory-space: 2.0.1 micromark-factory-space: 2.0.1
micromark-util-character: 2.1.1 micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.1 micromark-util-symbol: 2.0.1
@@ -9860,7 +9816,7 @@ snapshots:
'@types/katex': 0.16.8 '@types/katex': 0.16.8
hast-util-from-html-isomorphic: 2.0.0 hast-util-from-html-isomorphic: 2.0.0
hast-util-to-text: 4.0.2 hast-util-to-text: 4.0.2
katex: 0.16.27 katex: 0.16.28
unist-util-visit-parents: 6.0.2 unist-util-visit-parents: 6.0.2
vfile: 6.0.3 vfile: 6.0.3
@@ -10163,7 +10119,7 @@ snapshots:
streamdown@1.4.0(@types/react@19.2.8)(react@19.2.3): streamdown@1.4.0(@types/react@19.2.8)(react@19.2.3):
dependencies: dependencies:
clsx: 2.1.1 clsx: 2.1.1
katex: 0.16.27 katex: 0.16.28
lucide-react: 0.542.0(react@19.2.3) lucide-react: 0.542.0(react@19.2.3)
marked: 16.4.2 marked: 16.4.2
mermaid: 11.12.2 mermaid: 11.12.2

View File

@@ -7,7 +7,10 @@ import {
SquareArrowOutUpRightIcon, SquareArrowOutUpRightIcon,
XIcon, XIcon,
} from "lucide-react"; } from "lucide-react";
import * as React from "react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import rehypeKatex from "rehype-katex";
import remarkMath from "remark-math";
import { toast } from "sonner"; import { toast } from "sonner";
import { Streamdown } from "streamdown"; import { Streamdown } from "streamdown";
@@ -26,6 +29,12 @@ import {
} from "@/components/ai-elements/inline-citation"; } from "@/components/ai-elements/inline-citation";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { HoverCardTrigger } from "@/components/ui/hover-card"; import { HoverCardTrigger } from "@/components/ui/hover-card";
import {
buildCitationMap,
extractDomainFromUrl,
parseCitations,
type Citation,
} from "@/core/citations";
import { Select, SelectItem } from "@/components/ui/select"; import { Select, SelectItem } from "@/components/ui/select";
import { import {
SelectContent, SelectContent,
@@ -37,7 +46,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 { extractDomainFromUrl } from "@/core/citations";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { checkCodeFile, getFileName } from "@/core/utils/files"; import { checkCodeFile, getFileName } from "@/core/utils/files";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -81,6 +89,15 @@ export function ArtifactFileDetail({
filepath: filepathFromProps, filepath: filepathFromProps,
enabled: isCodeFile && !isWriteFile, enabled: isCodeFile && !isWriteFile,
}); });
// Parse citations and get clean content for code editor
const cleanContent = useMemo(() => {
if (language === "markdown" && content) {
return parseCitations(content).cleanContent;
}
return content;
}, [content, language]);
const [viewMode, setViewMode] = useState<"code" | "preview">("code"); const [viewMode, setViewMode] = useState<"code" | "preview">("code");
useEffect(() => { useEffect(() => {
if (previewable) { if (previewable) {
@@ -196,7 +213,7 @@ export function ArtifactFileDetail({
{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={content ?? ""} value={cleanContent ?? ""}
readonly readonly
/> />
)} )}
@@ -222,11 +239,23 @@ export function ArtifactFilePreview({
content: string; content: string;
language: string; language: string;
}) { }) {
const { citations, cleanContent, citationMap } = React.useMemo(() => {
const parsed = parseCitations(content ?? "");
const map = buildCitationMap(parsed.citations);
return {
citations: parsed.citations,
cleanContent: parsed.cleanContent,
citationMap: map,
};
}, [content]);
if (language === "markdown") { if (language === "markdown") {
return ( return (
<div className="size-full px-4"> <div className="size-full px-4">
<Streamdown <Streamdown
className="size-full" className="size-full"
remarkPlugins={[[remarkMath, { singleDollarTextMath: true }]]}
rehypePlugins={[[rehypeKatex, { output: "html" }]]}
components={{ components={{
a: ({ a: ({
href, href,
@@ -236,6 +265,16 @@ export function ArtifactFilePreview({
return <span>{children}</span>; return <span>{children}</span>;
} }
// Check if it's a citation link
const citation = citationMap.get(href);
if (citation) {
return (
<ArtifactCitationLink citation={citation} href={href}>
{children}
</ArtifactCitationLink>
);
}
// Check if it's an external link (http/https) // Check if it's an external link (http/https)
const isExternalLink = const isExternalLink =
href.startsWith("http://") || href.startsWith("https://"); href.startsWith("http://") || href.startsWith("https://");
@@ -255,7 +294,7 @@ export function ArtifactFilePreview({
}, },
}} }}
> >
{content ?? ""} {cleanContent ?? ""}
</Streamdown> </Streamdown>
</div> </div>
); );
@@ -271,6 +310,61 @@ export function ArtifactFilePreview({
return null; return null;
} }
/**
* Citation link component for artifact preview (with full citation data)
*/
function ArtifactCitationLink({
citation,
href,
children,
}: {
citation: Citation;
href: string;
children: React.ReactNode;
}) {
const domain = extractDomainFromUrl(href);
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="mx-0.5 cursor-pointer gap-1 rounded-full px-2 py-0.5 text-xs font-normal hover:bg-secondary/80"
>
{children ?? domain}
<ExternalLinkIcon className="size-3" />
</Badge>
</a>
</HoverCardTrigger>
<InlineCitationCardBody>
<div className="p-3">
<InlineCitationSource
title={citation.title}
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>
);
}
/** /**
* External link badge component for artifact preview * External link badge component for artifact preview
*/ */

View File

@@ -1,7 +1,9 @@
import type { Message } from "@langchain/langgraph-sdk"; import type { Message } from "@langchain/langgraph-sdk";
import { ExternalLinkIcon, LinkIcon } from "lucide-react"; import { ExternalLinkIcon, FileIcon, LinkIcon } from "lucide-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { memo, useMemo } from "react"; import { memo, useMemo } from "react";
import rehypeKatex from "rehype-katex";
import remarkMath from "remark-math";
import { import {
InlineCitationCard, InlineCitationCard,
@@ -26,6 +28,8 @@ import {
import { import {
extractContentFromMessage, extractContentFromMessage,
extractReasoningContentFromMessage, extractReasoningContentFromMessage,
parseUploadedFiles,
type UploadedFile,
} from "@/core/messages/utils"; } from "@/core/messages/utils";
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -82,16 +86,26 @@ function MessageContent_({
isLoading?: boolean; isLoading?: boolean;
}) { }) {
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
const isHuman = message.type === "human";
// Extract and parse citations from message content // Extract and parse citations and uploaded files from message content
const { citations, cleanContent } = useMemo(() => { const { citations, cleanContent, uploadedFiles } = useMemo(() => {
const reasoningContent = extractReasoningContentFromMessage(message); const reasoningContent = extractReasoningContentFromMessage(message);
const rawContent = extractContentFromMessage(message); const rawContent = extractContentFromMessage(message);
if (!isLoading && reasoningContent && !rawContent) { if (!isLoading && reasoningContent && !rawContent) {
return { citations: [], cleanContent: reasoningContent }; return { citations: [], cleanContent: reasoningContent, uploadedFiles: [] };
} }
return parseCitations(rawContent ?? "");
}, [isLoading, message]); // For human messages, first parse uploaded files
if (isHuman && rawContent) {
const { files, cleanContent: contentWithoutFiles } = parseUploadedFiles(rawContent);
const { citations, cleanContent: finalContent } = parseCitations(contentWithoutFiles);
return { citations, cleanContent: finalContent, uploadedFiles: files };
}
const { citations, cleanContent } = parseCitations(rawContent ?? "");
return { citations, cleanContent, uploadedFiles: [] };
}, [isLoading, message, isHuman]);
// Build citation map for quick URL lookup // Build citation map for quick URL lookup
const citationMap = useMemo( const citationMap = useMemo(
@@ -103,75 +117,212 @@ function MessageContent_({
return ( return (
<AIElementMessageContent className={className}> <AIElementMessageContent className={className}>
{/* Citations list at the top */} {/* Uploaded files for human messages - show first */}
{citations.length > 0 && <CitationsList citations={citations} />} {uploadedFiles.length > 0 && thread_id && (
<UploadedFilesList files={uploadedFiles} threadId={thread_id} />
)}
<AIElementMessageResponse {/* Message content - always show if present */}
rehypePlugins={rehypePlugins} {cleanContent && (
components={{ <AIElementMessageResponse
a: ({ remarkPlugins={[[remarkMath, { singleDollarTextMath: true }]]}
href, rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: "html" }]]}
children, components={{
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => { a: ({
if (!href) { href,
return <span>{children}</span>; children,
} }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
if (!href) {
return <span>{children}</span>;
}
// Check if this link matches a citation // Check if this link matches a citation
const citation = citationMap.get(href); const citation = citationMap.get(href);
if (citation) { if (citation) {
return (
<CitationLink citation={citation} href={href}>
{children}
</CitationLink>
);
}
// Regular external link
return ( return (
<CitationLink citation={citation} href={href}> <a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary underline underline-offset-2 hover:no-underline"
>
{children} {children}
</CitationLink> </a>
); );
} },
img: ({ src, alt }: React.ImgHTMLAttributes<HTMLImageElement>) => {
// Regular external link if (!src) return null;
return ( if (typeof src !== "string") {
<a return (
href={href} <img
target="_blank" className="max-w-full overflow-hidden rounded-lg"
rel="noopener noreferrer" src={src}
className="text-primary underline underline-offset-2 hover:no-underline" alt={alt}
> />
{children} );
</a> }
); let url = src;
}, if (src.startsWith("/mnt/")) {
img: ({ src, alt }: React.ImgHTMLAttributes<HTMLImageElement>) => { url = resolveArtifactURL(src, thread_id);
if (!src) return null; }
if (typeof src !== "string") {
return ( return (
<img <a href={url} target="_blank" rel="noopener noreferrer">
className="max-w-full overflow-hidden rounded-lg" <img
src={src} className="max-w-full overflow-hidden rounded-lg"
alt={alt} src={url}
/> alt={alt}
/>
</a>
); );
} },
let url = src; }}
if (src.startsWith("/mnt/")) { >
url = resolveArtifactURL(src, thread_id); {cleanContent}
} </AIElementMessageResponse>
return ( )}
<a href={url} target="_blank" rel="noopener noreferrer">
<img
className="max-w-full overflow-hidden rounded-lg"
src={url}
alt={alt}
/>
</a>
);
},
}}
>
{cleanContent}
</AIElementMessageResponse>
</AIElementMessageContent> </AIElementMessageContent>
); );
} }
/**
* Get file type label from filename extension
*/
function getFileTypeLabel(filename: string): string {
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
const typeMap: Record<string, string> = {
json: "JSON",
csv: "CSV",
txt: "TXT",
md: "Markdown",
py: "Python",
js: "JavaScript",
ts: "TypeScript",
tsx: "TSX",
jsx: "JSX",
html: "HTML",
css: "CSS",
xml: "XML",
yaml: "YAML",
yml: "YAML",
pdf: "PDF",
png: "PNG",
jpg: "JPG",
jpeg: "JPEG",
gif: "GIF",
svg: "SVG",
zip: "ZIP",
tar: "TAR",
gz: "GZ",
};
return typeMap[ext] || ext.toUpperCase() || "FILE";
}
/**
* Check if a file is an image based on extension
*/
function isImageFile(filename: string): boolean {
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
return ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"].includes(ext);
}
/**
* Uploaded files list component that displays files as cards or image thumbnails (Claude-style)
*/
function UploadedFilesList({
files,
threadId,
}: {
files: UploadedFile[];
threadId: string;
}) {
if (files.length === 0) return null;
return (
<div className="mb-2 flex flex-wrap gap-2">
{files.map((file, index) => (
<UploadedFileCard
key={`${file.path}-${index}`}
file={file}
threadId={threadId}
/>
))}
</div>
);
}
/**
* Single uploaded file card component (Claude-style)
* Shows image thumbnail for image files, file card for others
*/
function UploadedFileCard({
file,
threadId,
}: {
file: UploadedFile;
threadId: string;
}) {
const typeLabel = getFileTypeLabel(file.filename);
const isImage = isImageFile(file.filename);
// Don't render if threadId is invalid
if (!threadId) {
return null;
}
// Build URL - browser will handle encoding automatically
const imageUrl = resolveArtifactURL(file.path, threadId);
// For image files, show thumbnail
if (isImage) {
return (
<a
href={imageUrl}
target="_blank"
rel="noopener noreferrer"
className="group relative block overflow-hidden rounded-lg border"
>
<img
src={imageUrl}
alt={file.filename}
className="h-32 w-auto max-w-[240px] object-cover transition-transform group-hover:scale-105"
/>
</a>
);
}
// For non-image files, show file card
return (
<div className="bg-background flex min-w-[120px] max-w-[200px] flex-col gap-1 rounded-lg border p-3 shadow-sm">
<div className="flex items-start gap-2">
<FileIcon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
<span
className="text-foreground truncate text-sm font-medium"
title={file.filename}
>
{file.filename}
</span>
</div>
<div className="flex items-center justify-between gap-2">
<Badge
variant="secondary"
className="rounded px-1.5 py-0.5 text-[10px] font-normal"
>
{typeLabel}
</Badge>
<span className="text-muted-foreground text-[10px]">{file.size}</span>
</div>
</div>
);
}
/** /**
* Citations list component that displays all sources at the top * Citations list component that displays all sources at the top
*/ */

View File

@@ -33,41 +33,42 @@ export function parseCitations(content: string): ParseCitationsResult {
return { citations: [], cleanContent: content }; return { citations: [], cleanContent: content };
} }
// Match the citations block at the start of content (with possible leading whitespace) // Match ALL citations blocks anywhere in content (not just at the start)
const citationsRegex = /^\s*<citations>([\s\S]*?)<\/citations>/; const citationsRegex = /<citations>([\s\S]*?)<\/citations>/g;
const match = citationsRegex.exec(content);
if (!match) {
return { citations: [], cleanContent: content };
}
const citationsBlock = match[1] ?? "";
const citations: Citation[] = []; const citations: Citation[] = [];
const seenUrls = new Set<string>(); // Deduplicate by URL
let cleanContent = content;
// Parse each line as JSON let match;
const lines = citationsBlock.split("\n"); while ((match = citationsRegex.exec(content)) !== null) {
for (const line of lines) { const citationsBlock = match[1] ?? "";
const trimmed = line.trim();
if (trimmed?.startsWith("{")) { // Parse each line as JSON
try { const lines = citationsBlock.split("\n");
const citation = JSON.parse(trimmed) as Citation; for (const line of lines) {
// Validate required fields const trimmed = line.trim();
if (citation.id && citation.url) { if (trimmed?.startsWith("{")) {
citations.push({ try {
id: citation.id, const citation = JSON.parse(trimmed) as Citation;
title: citation.title || "", // Validate required fields and deduplicate
url: citation.url, if (citation.id && citation.url && !seenUrls.has(citation.url)) {
snippet: citation.snippet || "", seenUrls.add(citation.url);
}); citations.push({
id: citation.id,
title: citation.title || "",
url: citation.url,
snippet: citation.snippet || "",
});
}
} catch {
// Skip invalid JSON lines - this can happen during streaming
} }
} catch {
// Skip invalid JSON lines - this can happen during streaming
} }
} }
} }
// Remove the citations block from content // Remove ALL citations blocks from content
const cleanContent = content.replace(citationsRegex, "").trim(); cleanContent = content.replace(/<citations>[\s\S]*?<\/citations>/g, "").trim();
return { citations, cleanContent }; return { citations, cleanContent };
} }

View File

@@ -217,3 +217,58 @@ export function findToolCallResult(toolCallId: string, messages: Message[]) {
} }
return undefined; return undefined;
} }
/**
* Represents an uploaded file parsed from the <uploaded_files> tag
*/
export interface UploadedFile {
filename: string;
size: string;
path: string;
}
/**
* Result of parsing uploaded files from message content
*/
export interface ParsedUploadedFiles {
files: UploadedFile[];
cleanContent: string;
}
/**
* Parse <uploaded_files> tag from message content and extract file information.
* Returns the list of uploaded files and the content with the tag removed.
*/
export function parseUploadedFiles(content: string): ParsedUploadedFiles {
// Match <uploaded_files>...</uploaded_files> tag
const uploadedFilesRegex = /<uploaded_files>([\s\S]*?)<\/uploaded_files>/;
const match = content.match(uploadedFilesRegex);
if (!match) {
return { files: [], cleanContent: content };
}
const uploadedFilesContent = match[1];
const cleanContent = content.replace(uploadedFilesRegex, "").trim();
// Check if it's "No files have been uploaded yet."
if (uploadedFilesContent.includes("No files have been uploaded yet.")) {
return { files: [], cleanContent };
}
// Parse file list
// Format: - filename (size)\n Path: /path/to/file
const fileRegex = /- ([^\n(]+)\s*\(([^)]+)\)\s*\n\s*Path:\s*([^\n]+)/g;
const files: UploadedFile[] = [];
let fileMatch;
while ((fileMatch = fileRegex.exec(uploadedFilesContent)) !== null) {
files.push({
filename: fileMatch[1].trim(),
size: fileMatch[2].trim(),
path: fileMatch[3].trim(),
});
}
return { files, cleanContent };
}

View File

@@ -1,5 +1,6 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@import "katex/dist/katex.min.css";
@source "../node_modules/streamdown/dist/index.js"; @source "../node_modules/streamdown/dist/index.js";