Merge upstream/experimental into feat/citations

Resolved conflicts:
- backend/src/gateway/routers/artifacts.py: Keep citations block removal for markdown downloads
- frontend/src/components/workspace/messages/message-list-item.tsx: Keep improved citation handling with rehypePlugins, humanMessagePlugins, and CitationsLoadingIndicator

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
LofiSu
2026-02-07 00:53:16 +08:00
65 changed files with 3489 additions and 5320 deletions

100
frontend/AGENTS.md Normal file
View File

@@ -0,0 +1,100 @@
# Agents Architecture
## Overview
DeerFlow is built on a sophisticated agent-based architecture using the [LangGraph SDK](https://github.com/langchain-ai/langgraph) to enable intelligent, stateful AI interactions. This document outlines the agent system architecture, patterns, and best practices for working with agents in the frontend application.
## Architecture Overview
### Core Components
```
┌────────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
├────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ UI Components│───▶│ Thread Hooks │───▶│ LangGraph│ │
│ │ │ │ │ │ SDK │ │
│ └──────────────┘ └──────────────┘ └──────────┘ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ └───────────▶│ Thread State │◀──────────┘ │
│ │ Management │ │
│ └──────────────┘ │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ LangGraph Backend (lead_agent) │
│ ┌────────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │Main Agent │─▶│Sub-Agents│─▶│ Tools & Skills │ │
│ └────────────┘ └──────────┘ └───────────────────┘ │
└────────────────────────────────────────────────────────┘
```
## Project Structure
```
src/
├── app/ # Next.js App Router pages
│ ├── api/ # API routes
│ ├── workspace/ # Main workspace pages
│ └── mock/ # Mock/demo pages
├── components/ # React components
│ ├── ui/ # Reusable UI components
│ ├── workspace/ # Workspace-specific components
│ ├── landing/ # Landing page components
│ └── ai-elements/ # AI-related UI elements
├── core/ # Core business logic
│ ├── api/ # API client & data fetching
│ ├── artifacts/ # Artifact management
│ ├── citations/ # Citation handling
│ ├── config/ # App configuration
│ ├── i18n/ # Internationalization
│ ├── mcp/ # MCP integration
│ ├── messages/ # Message handling
│ ├── models/ # Data models & types
│ ├── settings/ # User settings
│ ├── skills/ # Skills system
│ ├── threads/ # Thread management
│ ├── todos/ # Todo system
│ └── utils/ # Utility functions
├── hooks/ # Custom React hooks
├── lib/ # Shared libraries & utilities
├── server/ # Server-side code (Not available yet)
│ └── better-auth/ # Authentication setup (Not available yet)
└── styles/ # Global styles
```
### Technology Stack
- **LangGraph SDK** (`@langchain/langgraph-sdk@1.5.3`) - Agent orchestration and streaming
- **LangChain Core** (`@langchain/core@1.1.15`) - Fundamental AI building blocks
- **TanStack Query** (`@tanstack/react-query@5.90.17`) - Server state management
- **React Hooks** - Thread lifecycle and state management
- **Shadcn UI** - UI components
- **MagicUI** - Magic UI components
- **React Bits** - React bits components
## Resources
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
- [LangChain Core Concepts](https://js.langchain.com/docs/concepts)
- [TanStack Query Documentation](https://tanstack.com/query/latest)
- [Next.js App Router](https://nextjs.org/docs/app)
## Contributing
When adding new agent features:
1. Follow the established project structure
2. Add comprehensive TypeScript types
3. Implement proper error handling
4. Write tests for new functionality
5. Update this documentation
6. Follow the code style guide (ESLint + Prettier)
## License
This agent architecture is part of the DeerFlow project.

89
frontend/CLAUDE.md Normal file
View File

@@ -0,0 +1,89 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
DeerFlow Frontend is a Next.js 16 web interface for an AI agent system. It communicates with a LangGraph-based backend to provide thread-based AI conversations with streaming responses, artifacts, and a skills/tools system.
**Stack**: Next.js 16, React 19, TypeScript 5.8, Tailwind CSS 4, pnpm 10.26.2
## Commands
| Command | Purpose |
|---------|---------|
| `pnpm dev` | Dev server with Turbopack (http://localhost:3000) |
| `pnpm build` | Production build |
| `pnpm check` | Lint + type check (run before committing) |
| `pnpm lint` | ESLint only |
| `pnpm lint:fix` | ESLint with auto-fix |
| `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) |
| `pnpm start` | Start production server |
No test framework is configured.
## Architecture
```
Frontend (Next.js) ──▶ LangGraph SDK ──▶ LangGraph Backend (lead_agent)
├── Sub-Agents
└── 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**.
### Source Layout (`src/`)
- **`app/`** — Next.js App Router. Routes: `/` (landing), `/workspace/chats/[thread_id]` (chat).
- **`components/`** — React components split into:
- `ui/` — Shadcn UI primitives (auto-generated, ESLint-ignored)
- `ai-elements/` — Vercel AI SDK elements (auto-generated, ESLint-ignored)
- `workspace/` — Chat page components (messages, artifacts, settings)
- `landing/` — Landing page sections
- **`core/`** — Business logic, the heart of the app:
- `threads/` — Thread creation, streaming, state management (hooks + types)
- `api/` — LangGraph client singleton
- `artifacts/` — Artifact loading and caching
- `i18n/` — Internationalization (en-US, zh-CN)
- `settings/` — User preferences in localStorage
- `memory/` — Persistent user memory system
- `skills/` — Skills installation and management
- `messages/` — Message processing and transformation
- `mcp/` — Model Context Protocol integration
- `models/` — TypeScript types and data models
- **`hooks/`** — Shared React hooks
- **`lib/`** — Utilities (`cn()` from clsx + tailwind-merge)
- **`server/`** — Server-side code (better-auth, not yet active)
- **`styles/`** — Global CSS with Tailwind v4 `@import` syntax and CSS variables for theming
### Data Flow
1. User input → thread hooks (`core/threads/hooks.ts`) → LangGraph SDK streaming
2. Stream events update thread state (messages, artifacts, todos)
3. TanStack Query manages server state; localStorage stores user settings
4. Components subscribe to thread state and render updates
### Key Patterns
- **Server Components by default**, `"use client"` only for interactive components
- **Thread hooks** (`useThreadStream`, `useSubmitThread`, `useThreads`) are the primary API interface
- **LangGraph client** is a singleton obtained via `getAPIClient()` in `core/api/`
- **Environment validation** uses `@t3-oss/env-nextjs` with Zod schemas (`src/env.js`). Skip with `SKIP_ENV_VALIDATION=1`
## Code Style
- **Imports**: Enforced ordering (builtin → external → internal → parent → sibling), alphabetized, newlines between groups. Use inline type imports: `import { type Foo }`.
- **Unused variables**: Prefix with `_`.
- **Class names**: Use `cn()` from `@/lib/utils` for conditional Tailwind classes.
- **Path alias**: `@/*` maps to `src/*`.
- **Components**: `ui/` and `ai-elements/` are generated from registries (Shadcn, MagicUI, React Bits, Vercel AI SDK) — don't manually edit these.
## Environment
Backend API URLs are optional; an nginx proxy is used by default:
```
NEXT_PUBLIC_BACKEND_BASE_URL=http://localhost:8001
NEXT_PUBLIC_LANGGRAPH_BASE_URL=http://localhost:2024
```
Requires Node.js 22+ and pnpm 10.26.2+.

View File

@@ -7,6 +7,15 @@ import "./src/env.js";
/** @type {import("next").NextConfig} */
const config = {
devIndicators: false,
turbopack: {
root: import.meta.dirname,
rules: {
"*.md": {
loaders: ["raw-loader"],
as: "*.js",
},
},
},
};
export default config;

View File

@@ -51,6 +51,7 @@
"ai": "^6.0.33",
"best-effort-json-parser": "^1.2.1",
"better-auth": "^1.3",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -96,6 +97,7 @@
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"raw-loader": "^4.0.2",
"tailwindcss": "^4.0.15",
"tw-animate-css": "^1.4.0",
"typescript": "^5.8.2",

579
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,363 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>棋圣聂卫平 - 永恒的围棋传奇</title>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&family=Noto+Serif+SC:wght@400;700;900&family=ZCOOL+QingKe+HuangYou&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚫</text></svg>">
</head>
<body>
<!-- 水墨背景效果 -->
<div class="ink-background"></div>
<div class="ink-splatter"></div>
<!-- 导航栏 -->
<nav class="main-nav">
<div class="nav-container">
<div class="nav-logo">
<span class="go-stone black"></span>
<h1>棋圣聂卫平</h1>
<span class="go-stone white"></span>
</div>
<ul class="nav-menu">
<li><a href="#home" class="nav-link">首页</a></li>
<li><a href="#life" class="nav-link">生平</a></li>
<li><a href="#achievements" class="nav-link">成就</a></li>
<li><a href="#gallery" class="nav-link">棋局</a></li>
<li><a href="#candle" class="nav-link">点蜡烛</a></li>
<li><a href="#legacy" class="nav-link">传承</a></li>
</ul>
<button class="nav-toggle" aria-label="导航菜单">
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</button>
</div>
</nav>
<!-- 主内容区域 -->
<main>
<!-- 英雄区域 -->
<section id="home" class="hero">
<div class="hero-content">
<div class="hero-text">
<h2 class="hero-title">一代<span class="highlight">棋圣</span></h2>
<h3 class="hero-subtitle">1952 - 2026</h3>
<p class="hero-quote">"只要是对围棋有益的事,我都愿意倾力去做。"</p>
<div class="hero-buttons">
<a href="#life" class="btn btn-primary">探索传奇</a>
<a href="#achievements" class="btn btn-outline">围棋成就</a>
</div>
</div>
<div class="hero-image">
<div class="portrait-frame">
<img src="https://imgcdn.yicai.com/uppics/images/2026/01/0366fe347acc0e54c6183eb0c9203e51.jpg" alt="聂卫平黑白肖像" class="portrait">
<div class="frame-decoration"></div>
</div>
</div>
</div>
<div class="scroll-indicator">
<span class="scroll-text">向下探索</span>
<div class="scroll-line"></div>
</div>
</section>
<!-- 生平介绍 -->
<section id="life" class="section life-section">
<div class="section-header">
<h2 class="section-title">生平轨迹</h2>
<div class="section-subtitle">黑白之间,落子无悔</div>
<div class="section-divider">
<span class="divider-line"></span>
<span class="divider-icon"></span>
<span class="divider-line"></span>
</div>
</div>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-date">1952</div>
<div class="timeline-content">
<h3>生于北京</h3>
<p>聂卫平出生于北京,童年时期受家庭熏陶开始接触围棋。</p>
</div>
<div class="timeline-marker">
<div class="marker-circle"></div>
<div class="marker-line"></div>
</div>
</div>
<div class="timeline-item">
<div class="timeline-date">1962</div>
<div class="timeline-content">
<h3>初露锋芒</h3>
<p>在北京六城市少儿围棋邀请赛中获得儿童组第三名,从陈毅元帅手中接过景泰蓝奖杯。</p>
</div>
<div class="timeline-marker">
<div class="marker-circle"></div>
<div class="marker-line"></div>
</div>
</div>
<div class="timeline-item">
<div class="timeline-date">1973</div>
<div class="timeline-content">
<h3>入选国家队</h3>
<p>中国棋院重建21岁的聂卫平入选围棋集训队开始职业棋手生涯。</p>
</div>
<div class="timeline-marker">
<div class="marker-circle"></div>
<div class="marker-line"></div>
</div>
</div>
<div class="timeline-item">
<div class="timeline-date">1984-1988</div>
<div class="timeline-content">
<h3>中日擂台赛奇迹</h3>
<p>在中日围棋擂台赛上创造11连胜神话打破日本围棋"不可战胜"的神话,被授予"棋圣"称号。</p>
</div>
<div class="timeline-marker">
<div class="marker-circle"></div>
<div class="marker-line"></div>
</div>
</div>
<div class="timeline-item">
<div class="timeline-date">2013</div>
<div class="timeline-content">
<h3>战胜病魔</h3>
<p>被查出罹患癌症,以乐观态度顽强与病魔作斗争,痊愈后继续为围棋事业奔波。</p>
</div>
<div class="timeline-marker">
<div class="marker-circle"></div>
<div class="marker-line"></div>
</div>
</div>
<div class="timeline-item">
<div class="timeline-date">2026</div>
<div class="timeline-content">
<h3>棋圣远行</h3>
<p>2026年1月14日聂卫平在北京逝世享年74岁一代棋圣落下人生最后一子。</p>
</div>
<div class="timeline-marker">
<div class="marker-circle"></div>
<div class="marker-line"></div>
</div>
</div>
</div>
</section>
<!-- 主要成就 -->
<section id="achievements" class="section achievements-section">
<div class="section-header">
<h2 class="section-title">辉煌成就</h2>
<div class="section-subtitle">一子定乾坤,十一连胜铸传奇</div>
<div class="section-divider">
<span class="divider-line"></span>
<span class="divider-icon"></span>
<span class="divider-line"></span>
</div>
</div>
<div class="achievements-grid">
<div class="achievement-card">
<div class="achievement-icon">
<i class="fas fa-trophy"></i>
</div>
<h3>棋圣称号</h3>
<p>1988年被授予"棋圣"称号,这是中国围棋界的最高荣誉,至今独此一人。</p>
</div>
<div class="achievement-card">
<div class="achievement-icon">
<i class="fas fa-flag"></i>
</div>
<h3>中日擂台赛11连胜</h3>
<p>在中日围棋擂台赛上创造11连胜神话极大振奋了民族精神和自信心。</p>
</div>
<div class="achievement-card">
<div class="achievement-icon">
<i class="fas fa-users"></i>
</div>
<h3>人才培养</h3>
<p>培养常昊、古力、柯洁等20多位世界冠军近300名职业棋手。</p>
</div>
<div class="achievement-card">
<div class="achievement-icon">
<i class="fas fa-globe-asia"></i>
</div>
<h3>围棋推广</h3>
<p>推动围棋从专业走向大众,"聂旋风"席卷全国,极大增加了围棋人口。</p>
</div>
</div>
<div class="stats-container">
<div class="stat-item">
<div class="stat-number" data-count="11">0</div>
<div class="stat-label">擂台赛连胜</div>
</div>
<div class="stat-item">
<div class="stat-number" data-count="74">0</div>
<div class="stat-label">人生岁月</div>
</div>
<div class="stat-item">
<div class="stat-number" data-count="300">0</div>
<div class="stat-label">培养棋手</div>
</div>
<div class="stat-item">
<div class="stat-number" data-count="40">0</div>
<div class="stat-label">围棋生涯</div>
</div>
</div>
</section>
<!-- 围棋棋盘展示 -->
<section id="gallery" class="section gallery-section">
<div class="section-header">
<h2 class="section-title">经典棋局</h2>
<div class="section-subtitle">纵横十九道,妙手定乾坤</div>
<div class="section-divider">
<span class="divider-line"></span>
<span class="divider-icon"></span>
<span class="divider-line"></span>
</div>
</div>
<div class="go-board-container">
<div class="go-board">
<!-- 围棋棋盘网格 -->
<div class="board-grid"></div>
<!-- 经典棋局棋子 -->
<div class="board-stones">
<!-- 这里将通过JavaScript动态生成棋子 -->
</div>
<div class="board-info">
<h3>1985年首届中日擂台赛决胜局</h3>
<p>聂卫平执黑3目半击败日本队主将藤泽秀行打破日本围棋"不可战胜"的神话。</p>
</div>
</div>
</div>
<div class="game-quotes">
<blockquote class="game-quote">
<p>"我是从乒乓球队借的衣服,当时我想自己代表中国来比赛,你不能输,我也不能输,人生能有几回搏,那就分个高低吧。"</p>
<footer>—— 聂卫平谈首届擂台赛</footer>
</blockquote>
</div>
</section>
<!-- 蜡烛纪念 -->
<section id="candle" class="section candle-section">
<div class="section-header">
<h2 class="section-title">点亮心灯</h2>
<div class="section-subtitle">一烛一缅怀,光明永相传</div>
<div class="section-divider">
<span class="divider-line"></span>
<span class="divider-icon">🕯️</span>
<span class="divider-line"></span>
</div>
</div>
<div class="candle-container">
<div class="candle-instructions">
<p>点击下方的蜡烛,为棋圣聂卫平点亮一盏心灯,表达您的缅怀之情。</p>
<div class="candle-stats">
<div class="candle-count">
<span class="count-number">0</span>
<span class="count-label">盏蜡烛已点亮</span>
</div>
<div class="candle-message">
<span class="message-text">您的缅怀将永远铭记</span>
</div>
</div>
</div>
<div class="candle-grid">
<!-- 蜡烛将通过JavaScript动态生成 -->
</div>
<div class="candle-controls">
<button class="btn btn-primary light-candle-btn">
<i class="fas fa-fire"></i>
点亮蜡烛
</button>
<button class="btn btn-outline reset-candles-btn">
<i class="fas fa-redo"></i>
重置蜡烛
</button>
<button class="btn btn-outline auto-light-btn">
<i class="fas fa-magic"></i>
自动点亮
</button>
</div>
<div class="candle-quote">
<blockquote>
<p>"棋盘上的道理对于日常生活、学习工作都有指导作用。即使在AI时代人类仍需要围棋。"</p>
<footer>—— 聂卫平</footer>
</blockquote>
</div>
</div>
</section>
<!-- 传承与影响 -->
<section id="legacy" class="section legacy-section">
<div class="section-header">
<h2 class="section-title">精神传承</h2>
<div class="section-subtitle">棋魂永驻,精神不朽</div>
<div class="section-divider">
<span class="divider-line"></span>
<span class="divider-icon"></span>
<span class="divider-line"></span>
</div>
</div>
<div class="legacy-content">
<div class="legacy-text">
<h3>超越时代的棋圣</h3>
<p>聂卫平的一生是传奇的一生、热爱的一生、奉献的一生。他崛起于中国改革开放初期,他的胜利不仅是体育成就,更是民族自信的象征。</p>
<p>他打破了日本围棋的垄断,推动世界棋坛进入中日韩三国鼎立时代,为中国围棋从追赶到领先奠定了基础。他让围棋这项中华古老技艺重新焕发生机,成为连接传统与现代的文化桥梁。</p>
<p>即便在AI改变围棋的今天聂卫平所代表的人类智慧、意志力和文化传承的价值依然不可或缺。他下完了自己的人生棋局但留下的"棋魂"将永远在中国围棋史上熠熠生辉。</p>
</div>
<div class="legacy-image">
<div class="ink-painting">
<div class="painting-stroke"></div>
<div class="painting-stroke"></div>
<div class="painting-stroke"></div>
<div class="painting-text">棋如人生</div>
</div>
</div>
</div>
</section>
<!-- 页脚 -->
<footer class="main-footer">
<div class="footer-content">
<div class="footer-logo">
<span class="go-stone black"></span>
<span>棋圣聂卫平</span>
<span class="go-stone white"></span>
</div>
<p class="footer-quote">"棋盘上的道理对于日常生活、学习工作都有指导作用。即使在AI时代人类仍需要围棋。"</p>
<div class="footer-links">
<a href="#home">首页</a>
<a href="#life">生平</a>
<a href="#achievements">成就</a>
<a href="#gallery">棋局</a>
<a href="#legacy">传承</a>
</div>
<div class="footer-copyright">
<p>© 2026 纪念棋圣聂卫平 | 永恒的围棋传奇</p>
<a href="https://deerflow.tech" target="_blank" class="deerflow-badge">Created By Deerflow</a>
</div>
</div>
</footer>
</main>
<!-- 返回顶部按钮 -->
<button class="back-to-top" aria-label="返回顶部">
<i class="fas fa-chevron-up"></i>
</button>
<!-- 围棋棋子浮动效果 -->
<div class="floating-stones">
<div class="floating-stone black"></div>
<div class="floating-stone white"></div>
<div class="floating-stone black"></div>
<div class="floating-stone white"></div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -1,646 +0,0 @@
// 聂卫平纪念网站 - 交互效果
document.addEventListener('DOMContentLoaded', function() {
// 初始化
initNavigation();
initScrollEffects();
initStatsCounter();
initGoBoard();
initBackToTop();
initAnimations();
initCandleMemorial(); // 初始化蜡烛纪念功能
console.log('棋圣聂卫平纪念网站已加载 - 永恒的围棋传奇');
});
// 导航菜单功能
function initNavigation() {
const navToggle = document.querySelector('.nav-toggle');
const navMenu = document.querySelector('.nav-menu');
const navLinks = document.querySelectorAll('.nav-link');
// 切换移动端菜单
navToggle.addEventListener('click', function() {
navMenu.classList.toggle('active');
navToggle.classList.toggle('active');
});
// 点击导航链接时关闭菜单
navLinks.forEach(link => {
link.addEventListener('click', function() {
navMenu.classList.remove('active');
navToggle.classList.remove('active');
});
});
// 滚动时高亮当前部分
window.addEventListener('scroll', highlightCurrentSection);
}
// 高亮当前滚动到的部分
function highlightCurrentSection() {
const sections = document.querySelectorAll('section');
const navLinks = document.querySelectorAll('.nav-link');
let currentSection = '';
sections.forEach(section => {
const sectionTop = section.offsetTop - 100;
const sectionHeight = section.clientHeight;
const scrollPosition = window.scrollY;
if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) {
currentSection = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${currentSection}`) {
link.classList.add('active');
}
});
}
// 滚动效果
function initScrollEffects() {
// 添加滚动时的淡入效果
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animated');
}
});
}, observerOptions);
// 观察需要动画的元素
const animatedElements = document.querySelectorAll('.timeline-item, .achievement-card, .game-quote, .legacy-text, .legacy-image');
animatedElements.forEach(el => observer.observe(el));
// 平滑滚动到锚点
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
const targetId = this.getAttribute('href');
if (targetId === '#') return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
e.preventDefault();
window.scrollTo({
top: targetElement.offsetTop - 80,
behavior: 'smooth'
});
}
});
});
}
// 统计数据计数器
function initStatsCounter() {
const statNumbers = document.querySelectorAll('.stat-number');
const observerOptions = {
threshold: 0.5
};
const observer = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const statNumber = entry.target;
const target = parseInt(statNumber.getAttribute('data-count'));
const duration = 2000; // 2秒
const increment = target / (duration / 16); // 60fps
let current = 0;
const timer = setInterval(() => {
current += increment;
if (current >= target) {
current = target;
clearInterval(timer);
}
statNumber.textContent = Math.floor(current);
}, 16);
observer.unobserve(statNumber);
}
});
}, observerOptions);
statNumbers.forEach(number => observer.observe(number));
}
// 围棋棋盘初始化
function initGoBoard() {
const boardStones = document.querySelector('.board-stones');
if (!boardStones) return;
// 经典棋局棋子位置 (模拟1985年决胜局)
const stonePositions = [
{ type: 'black', x: 4, y: 4 },
{ type: 'white', x: 4, y: 16 },
{ type: 'black', x: 16, y: 4 },
{ type: 'white', x: 16, y: 16 },
{ type: 'black', x: 10, y: 10 },
{ type: 'white', x: 9, y: 9 },
{ type: 'black', x: 3, y: 15 },
{ type: 'white', x: 15, y: 3 },
{ type: 'black', x: 17, y: 17 },
{ type: 'white', x: 2, y: 2 }
];
// 创建棋子
stonePositions.forEach((stone, index) => {
const stoneElement = document.createElement('div');
stoneElement.className = `board-stone ${stone.type}`;
// 计算位置 (19x19棋盘)
const xPercent = (stone.x / 18) * 100;
const yPercent = (stone.y / 18) * 100;
stoneElement.style.left = `${xPercent}%`;
stoneElement.style.top = `${yPercent}%`;
stoneElement.style.animationDelay = `${index * 0.2}s`;
boardStones.appendChild(stoneElement);
});
// 添加棋盘样式
const style = document.createElement('style');
style.textContent = `
.board-stone {
position: absolute;
width: 4%;
height: 4%;
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
animation: stoneAppear 0.5s ease-out forwards;
opacity: 0;
}
.board-stone.black {
background: radial-gradient(circle at 30% 30%, #555, #000);
}
.board-stone.white {
background: radial-gradient(circle at 30% 30%, #fff, #ddd);
border: 1px solid #aaa;
}
@keyframes stoneAppear {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
`;
document.head.appendChild(style);
}
// 返回顶部按钮
function initBackToTop() {
const backToTopBtn = document.querySelector('.back-to-top');
window.addEventListener('scroll', function() {
if (window.scrollY > 300) {
backToTopBtn.classList.add('visible');
} else {
backToTopBtn.classList.remove('visible');
}
});
backToTopBtn.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
// 初始化动画
function initAnimations() {
// 添加滚动时的水墨效果
let lastScrollTop = 0;
const inkSplatter = document.querySelector('.ink-splatter');
window.addEventListener('scroll', function() {
const scrollTop = window.scrollY;
const scrollDirection = scrollTop > lastScrollTop ? 'down' : 'up';
// 根据滚动方向调整水墨效果
if (inkSplatter) {
const opacity = 0.1 + (scrollTop / 5000);
inkSplatter.style.opacity = Math.min(opacity, 0.3);
// 轻微移动效果
const moveX = (scrollTop % 100) / 100;
inkSplatter.style.transform = `translateX(${moveX}px)`;
}
lastScrollTop = scrollTop;
});
// 鼠标移动时的墨水效果
document.addEventListener('mousemove', function(e) {
const floatingStones = document.querySelectorAll('.floating-stone');
floatingStones.forEach((stone, index) => {
const speed = 0.01 + (index * 0.005);
const x = (window.innerWidth - e.clientX) * speed;
const y = (window.innerHeight - e.clientY) * speed;
stone.style.transform = `translate(${x}px, ${y}px)`;
});
});
// 页面加载时的动画序列
setTimeout(() => {
document.body.classList.add('loaded');
}, 100);
}
// 添加键盘快捷键
document.addEventListener('keydown', function(e) {
// 空格键滚动
if (e.code === 'Space' && !e.target.matches('input, textarea')) {
e.preventDefault();
window.scrollBy({
top: window.innerHeight * 0.8,
behavior: 'smooth'
});
}
// ESC键返回顶部
if (e.code === 'Escape') {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
// 数字键跳转到对应部分
if (e.code >= 'Digit1' && e.code <= 'Digit5') {
const sectionIndex = parseInt(e.code.replace('Digit', '')) - 1;
const sections = ['home', 'life', 'achievements', 'gallery', 'legacy'];
if (sectionIndex < sections.length) {
const targetSection = document.getElementById(sections[sectionIndex]);
if (targetSection) {
window.scrollTo({
top: targetSection.offsetTop - 80,
behavior: 'smooth'
});
}
}
}
});
// 添加打印友好功能
window.addEventListener('beforeprint', function() {
document.body.classList.add('printing');
});
window.addEventListener('afterprint', function() {
document.body.classList.remove('printing');
});
// 性能优化:图片懒加载
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
imageObserver.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => imageObserver.observe(img));
}
// 添加触摸设备优化
if ('ontouchstart' in window) {
document.body.classList.add('touch-device');
// 为触摸设备调整悬停效果
const style = document.createElement('style');
style.textContent = `
.touch-device .achievement-card:hover {
transform: none;
}
.touch-device .btn:hover {
transform: none;
}
`;
document.head.appendChild(style);
}
// 添加页面可见性API支持
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
console.log('页面隐藏中...');
} else {
console.log('页面恢复显示');
}
});
// 错误处理
window.addEventListener('error', function(e) {
console.error('页面错误:', e.message);
});
// 蜡烛纪念功能
function initCandleMemorial() {
const candleGrid = document.querySelector('.candle-grid');
const lightCandleBtn = document.querySelector('.light-candle-btn');
const resetCandlesBtn = document.querySelector('.reset-candles-btn');
const autoLightBtn = document.querySelector('.auto-light-btn');
const countNumber = document.querySelector('.count-number');
const messageText = document.querySelector('.message-text');
if (!candleGrid) return;
// 蜡烛数量
const candleCount = 24; // 24支蜡烛象征24小时永恒纪念
let litCandles = 0;
let candles = [];
// 初始化蜡烛
function createCandles() {
candleGrid.innerHTML = '';
candles = [];
litCandles = 0;
for (let i = 0; i < candleCount; i++) {
const candle = document.createElement('div');
candle.className = 'candle-item';
candle.dataset.index = i;
candle.innerHTML = `
<div class="candle-flame">
<div class="flame-core"></div>
<div class="flame-outer"></div>
<div class="flame-spark"></div>
<div class="flame-spark"></div>
<div class="flame-spark"></div>
</div>
<div class="candle-body"></div>
`;
// 点击点亮/熄灭蜡烛
candle.addEventListener('click', function() {
toggleCandle(i);
});
candleGrid.appendChild(candle);
candles.push({
element: candle,
lit: false
});
}
updateCounter();
}
// 切换蜡烛状态
function toggleCandle(index) {
const candle = candles[index];
if (candle.lit) {
// 熄灭蜡烛
candle.element.classList.remove('candle-lit');
candle.lit = false;
litCandles--;
// 添加熄灭动画
candle.element.style.animation = 'none';
setTimeout(() => {
candle.element.style.animation = '';
}, 10);
} else {
// 点亮蜡烛
candle.element.classList.add('candle-lit');
candle.lit = true;
litCandles++;
// 添加点亮动画
candle.element.style.animation = 'candleLightUp 0.5s ease';
}
updateCounter();
updateMessage();
saveCandleState();
}
// 点亮一支蜡烛
function lightOneCandle() {
// 找到未点亮的蜡烛
const unlitCandles = candles.filter(c => !c.lit);
if (unlitCandles.length === 0) return false;
// 随机选择一支
const randomIndex = Math.floor(Math.random() * unlitCandles.length);
const candleIndex = candles.indexOf(unlitCandles[randomIndex]);
toggleCandle(candleIndex);
return true;
}
// 自动点亮所有蜡烛
function autoLightCandles() {
if (litCandles === candleCount) return;
let delay = 0;
for (let i = 0; i < candles.length; i++) {
if (!candles[i].lit) {
setTimeout(() => {
toggleCandle(i);
}, delay);
delay += 100; // 每100毫秒点亮一支
}
}
}
// 重置所有蜡烛
function resetAllCandles() {
candles.forEach((candle, index) => {
if (candle.lit) {
candle.element.classList.remove('candle-lit');
candle.lit = false;
// 添加重置动画
candle.element.style.animation = 'none';
setTimeout(() => {
candle.element.style.animation = '';
}, 10);
}
});
litCandles = 0;
updateCounter();
updateMessage();
saveCandleState();
}
// 更新计数器
function updateCounter() {
if (countNumber) {
countNumber.textContent = litCandles;
// 添加计数动画
countNumber.style.transform = 'scale(1.2)';
setTimeout(() => {
countNumber.style.transform = 'scale(1)';
}, 200);
}
}
// 更新消息
function updateMessage() {
if (!messageText) return;
const messages = [
"您的缅怀将永远铭记",
"一烛一缅怀,光明永相传",
"棋圣精神,永垂不朽",
"黑白之间,永恒追忆",
"围棋之光,永不熄灭",
"传承是最好的纪念"
];
// 根据点亮数量选择消息
let messageIndex;
if (litCandles === 0) {
messageIndex = 0;
} else if (litCandles < candleCount / 2) {
messageIndex = 1;
} else if (litCandles < candleCount) {
messageIndex = 2;
} else {
messageIndex = 3;
}
// 随机选择同级别的消息
const startIndex = Math.floor(messageIndex / 2) * 2;
const endIndex = startIndex + 2;
const availableMessages = messages.slice(startIndex, endIndex);
const randomMessage = availableMessages[Math.floor(Math.random() * availableMessages.length)];
messageText.textContent = randomMessage;
}
// 保存蜡烛状态到本地存储
function saveCandleState() {
try {
const candleState = candles.map(c => c.lit);
localStorage.setItem('nieCandleState', JSON.stringify(candleState));
localStorage.setItem('nieCandleCount', litCandles.toString());
} catch (e) {
console.log('无法保存蜡烛状态:', e);
}
}
// 加载蜡烛状态
function loadCandleState() {
try {
const savedState = localStorage.getItem('nieCandleState');
const savedCount = localStorage.getItem('nieCandleCount');
if (savedState) {
const candleState = JSON.parse(savedState);
candleState.forEach((isLit, index) => {
if (isLit && candles[index]) {
candles[index].element.classList.add('candle-lit');
candles[index].lit = true;
}
});
litCandles = savedCount ? parseInt(savedCount) : candleState.filter(Boolean).length;
updateCounter();
updateMessage();
}
} catch (e) {
console.log('无法加载蜡烛状态:', e);
}
}
// 初始化
createCandles();
// 加载保存的状态
setTimeout(() => {
loadCandleState();
}, 100);
// 按钮事件
if (lightCandleBtn) {
lightCandleBtn.addEventListener('click', function() {
if (!lightOneCandle()) {
// 所有蜡烛都已点亮
this.innerHTML = '<i class="fas fa-check"></i> 所有蜡烛已点亮';
this.disabled = true;
setTimeout(() => {
this.innerHTML = '<i class="fas fa-fire"></i> 点亮蜡烛';
this.disabled = false;
}, 2000);
}
});
}
if (resetCandlesBtn) {
resetCandlesBtn.addEventListener('click', function() {
if (confirm('确定要熄灭所有蜡烛吗?')) {
resetAllCandles();
}
});
}
if (autoLightBtn) {
autoLightBtn.addEventListener('click', function() {
autoLightCandles();
});
}
// 添加键盘快捷键
document.addEventListener('keydown', function(e) {
// C键点亮一支蜡烛
if (e.code === 'KeyC' && !e.target.matches('input, textarea')) {
e.preventDefault();
lightOneCandle();
}
// R键重置蜡烛
if (e.code === 'KeyR' && e.ctrlKey && !e.target.matches('input, textarea')) {
e.preventDefault();
resetAllCandles();
}
// A键自动点亮
if (e.code === 'KeyA' && e.ctrlKey && !e.target.matches('input, textarea')) {
e.preventDefault();
autoLightCandles();
}
});
console.log('蜡烛纪念功能已初始化');
}
// 页面卸载前的确认
window.addEventListener('beforeunload', function(e) {
// 可以在这里添加保存功能
});

View File

@@ -177,7 +177,8 @@ export default function ChatPage() {
threadContext: {
...settings.context,
thinking_enabled: settings.context.mode !== "flash",
is_plan_mode: settings.context.mode === "pro",
is_plan_mode: settings.context.mode === "pro" || settings.context.mode === "ultra",
subagent_enabled: settings.context.mode === "ultra",
},
afterSubmit() {
router.push(pathOfThread(threadId!));
@@ -244,7 +245,7 @@ export default function ChatPage() {
<div
className={cn(
"relative w-full",
isNewThread && "-translate-y-[calc(50vh-160px)]",
isNewThread && "-translate-y-[calc(50vh-96px)]",
isNewThread
? "max-w-(--container-width-sm)"
: "max-w-(--container-width-md)",

View File

@@ -14,7 +14,7 @@ export default function WorkspaceLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
const [settings, setSettings] = useLocalSettings();
const [open, setOpen] = useState(false);
const [open, setOpen] = useState(() => !settings.layout.sidebar_collapsed);
useEffect(() => {
setOpen(!settings.layout.sidebar_collapsed);
}, [settings.layout.sidebar_collapsed]);

View File

@@ -60,8 +60,7 @@ export function Hero({ className }: { className?: string }) {
className="mt-8 scale-105 text-center text-2xl text-shadow-sm"
style={{ color: "rgb(182,182,188)" }}
>
DeerFlow is an open-source SuperAgent that researches, codes, and
creates.
An open-source SuperAgent harness that researches, codes, and creates.
<br />
With the help of sandboxes, memories, tools and skills, it handles
<br />

View File

@@ -0,0 +1,49 @@
"use client";
import React, { type MouseEventHandler } from "react";
import confetti from "canvas-confetti";
import { Button } from "@/components/ui/button";
interface ConfettiButtonProps extends React.ComponentProps<typeof Button> {
angle?: number;
particleCount?: number;
startVelocity?: number;
spread?: number;
onClick?: MouseEventHandler<HTMLButtonElement>;
}
export function ConfettiButton({
className,
children,
angle = 90,
particleCount = 75,
startVelocity = 35,
spread = 70,
onClick,
...props
}: ConfettiButtonProps) {
const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => {
const target = event.currentTarget;
if (target) {
const rect = target.getBoundingClientRect();
confetti({
particleCount,
startVelocity,
angle,
spread,
origin: {
x: (rect.left + rect.width / 2) / window.innerWidth,
y: (rect.top + rect.height / 2) / window.innerHeight,
},
});
}
onClick?.(event);
};
return (
<Button onClick={handleClick} className={className} {...props}>
{children}
</Button>
);
}

View File

@@ -7,6 +7,8 @@ import {
LightbulbIcon,
PaperclipIcon,
PlusIcon,
SparklesIcon,
RocketIcon,
ZapIcon,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
@@ -30,6 +32,7 @@ import {
usePromptInputController,
type PromptInputMessage,
} from "@/components/ai-elements/prompt-input";
import { ConfettiButton } from "@/components/ui/confetti-button";
import {
DropdownMenuGroup,
DropdownMenuLabel,
@@ -78,9 +81,9 @@ export function InputBox({
disabled?: boolean;
context: Omit<
AgentThreadContext,
"thread_id" | "is_plan_mode" | "thinking_enabled"
"thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled"
> & {
mode: "flash" | "thinking" | "pro" | undefined;
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
};
extraHeader?: React.ReactNode;
isNewThread?: boolean;
@@ -88,9 +91,9 @@ export function InputBox({
onContextChange?: (
context: Omit<
AgentThreadContext,
"thread_id" | "is_plan_mode" | "thinking_enabled"
"thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled"
> & {
mode: "flash" | "thinking" | "pro" | undefined;
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
},
) => void;
onSubmit?: (message: PromptInputMessage) => void;
@@ -129,7 +132,7 @@ export function InputBox({
[onContextChange, context],
);
const handleModeSelect = useCallback(
(mode: "flash" | "thinking" | "pro") => {
(mode: "flash" | "thinking" | "pro" | "ultra") => {
onContextChange?.({
...context,
mode,
@@ -203,11 +206,15 @@ export function InputBox({
{context.mode === "pro" && (
<GraduationCapIcon className="size-3" />
)}
{context.mode === "ultra" && (
<RocketIcon className="size-3" />
)}
</div>
<div className="text-xs font-normal">
{(context.mode === "flash" && t.inputBox.flashMode) ||
(context.mode === "thinking" && t.inputBox.reasoningMode) ||
(context.mode === "pro" && t.inputBox.proMode)}
(context.mode === "pro" && t.inputBox.proMode) ||
(context.mode === "ultra" && t.inputBox.ultraMode)}
</div>
</PromptInputActionMenuTrigger>
<PromptInputActionMenuContent className="w-80">
@@ -304,6 +311,34 @@ export function InputBox({
<div className="ml-auto size-4" />
)}
</PromptInputActionMenuItem>
<PromptInputActionMenuItem
className={cn(
context.mode === "ultra"
? "text-accent-foreground"
: "text-muted-foreground/65",
)}
onSelect={() => handleModeSelect("ultra")}
>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-1 font-bold">
<RocketIcon
className={cn(
"mr-2 size-4",
context.mode === "ultra" && "text-accent-foreground",
)}
/>
{t.inputBox.ultraMode}
</div>
<div className="pl-7 text-xs">
{t.inputBox.ultraModeDescription}
</div>
</div>
{context.mode === "ultra" ? (
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</PromptInputActionMenuItem>
</PromptInputActionMenu>
</DropdownMenuGroup>
</PromptInputActionMenuContent>
@@ -386,6 +421,14 @@ function SuggestionList() {
);
return (
<Suggestions className="w-fit">
<ConfettiButton
className="text-muted-foreground cursor-pointer rounded-full px-4 text-xs font-normal"
variant="outline"
size="sm"
onClick={() => handleSuggestionClick(t.inputBox.surpriseMePrompt)}
>
<SparklesIcon className="size-4" /> {t.inputBox.surpriseMe}
</ConfettiButton>
{t.inputBox.suggestions.map((suggestion) => (
<Suggestion
key={suggestion.suggestion}

View File

@@ -220,11 +220,13 @@ function ToolCall({
{Array.isArray(result) && (
<ChainOfThoughtSearchResults>
{result.map((item) => (
<ChainOfThoughtSearchResult key={item.url}>
<a href={item.url} target="_blank" rel="noreferrer">
{item.title}
</a>
</ChainOfThoughtSearchResult>
<Tooltip key={item.url} content={item.snippet}>
<ChainOfThoughtSearchResult key={item.url}>
<a href={item.url} target="_blank" rel="noreferrer">
{item.title}
</a>
</ChainOfThoughtSearchResult>
</Tooltip>
))}
</ChainOfThoughtSearchResults>
)}

View File

@@ -285,7 +285,7 @@ function UploadedFilesList({ files, threadId }: { files: UploadedFile[]; threadI
if (files.length === 0) return null;
return (
<div className="mb-2 flex flex-wrap gap-2">
<div className="mb-2 flex flex-wrap justify-end gap-2">
{files.map((file, index) => (
<UploadedFileCard key={`${file.path}-${index}`} file={file} threadId={threadId} />
))}

View File

@@ -0,0 +1,9 @@
"use client";
import { Streamdown } from "streamdown";
import about from "./about.md";
export function AboutSettingsPage() {
return <Streamdown>{about}</Streamdown>;
}

View File

@@ -0,0 +1,52 @@
# 🦌 [About DeerFlow 2.0](https://github.com/bytedance/deer-flow)
> **From Open Source, Back to Open Source**
**DeerFlow** (**D**eep **E**xploration and **E**fficient **R**esearch **Flow**) is a community-driven SuperAgent harness that researches, codes, and creates.
With the help of sandboxes, memories, tools and skills, it handles
different levels of tasks that could take minutes to hours.
---
## 🌟 GitHub Repository
Explore DeerFlow on GitHub: [github.com/bytedance/deer-flow](https://github.com/bytedance/deer-flow)
## 🌐 Official Website
Visit the official website of DeerFlow: [deerflow.tech](https://deerflow.tech/)
## 📧 Support
If you have any questions or need help, please contact us at [support@deerflow.tech](mailto:support@deerflow.tech).
---
## 📜 License
DeerFlow is proudly open source and distributed under the **MIT License**.
---
## 🙌 Acknowledgments
We extend our heartfelt gratitude to the open source projects and contributors who have made DeerFlow a reality. We truly stand on the shoulders of giants.
### Core Frameworks
- **[LangChain](https://github.com/langchain-ai/langchain)**: A phenomenal framework that powers our LLM interactions and chains.
- **[LangGraph](https://github.com/langchain-ai/langgraph)**: Enabling sophisticated multi-agent orchestration.
- **[Next.js](https://nextjs.org/)**: A cutting-edge framework for building web applications.
### UI Libraries
- **[Shadcn](https://ui.shadcn.com/)**: Minimalistic components that power our UI.
- **[SToneX](https://github.com/stonexer)**: For his invaluable contribution to token-by-token visual effects.
These outstanding projects form the backbone of DeerFlow and exemplify the transformative power of open source collaboration.
### Special Thanks
Finally, we want to express our heartfelt gratitude to the core authors of DeerFlow 1.0 and 2.0:
- **[Daniel Walnut](https://github.com/hetaoBackend/)**
- **[Henry Li](https://github.com/magiccube/)**
Without their vision, passion and dedication, `DeerFlow` would not be what it is today.

View File

@@ -1,5 +0,0 @@
"use client";
export function AcknowledgePage() {
return null;
}

View File

@@ -38,7 +38,6 @@ function memoryToMarkdown(
const parts: string[] = [];
parts.push(`## ${t.settings.memory.markdown.overview}`);
parts.push(`- **${t.common.version}**: \`${memory.version}\``);
parts.push(
`- **${t.common.lastUpdated}**: \`${formatTimeAgo(memory.lastUpdated)}\``,
);

View File

@@ -2,12 +2,13 @@
import {
BellIcon,
InfoIcon,
BrainIcon,
PaletteIcon,
SparklesIcon,
WrenchIcon,
} from "lucide-react";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import {
Dialog,
@@ -16,7 +17,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { AcknowledgePage } from "@/components/workspace/settings/acknowledge-page";
import { AboutSettingsPage } from "@/components/workspace/settings/about-settings-page";
import { AppearanceSettingsPage } from "@/components/workspace/settings/appearance-settings-page";
import { MemorySettingsPage } from "@/components/workspace/settings/memory-settings-page";
import { NotificationSettingsPage } from "@/components/workspace/settings/notification-settings-page";
@@ -31,7 +32,7 @@ type SettingsSection =
| "tools"
| "skills"
| "notification"
| "acknowledge";
| "about";
type SettingsDialogProps = React.ComponentProps<typeof Dialog> & {
defaultSection?: SettingsSection;
@@ -43,6 +44,14 @@ export function SettingsDialog(props: SettingsDialogProps) {
const [activeSection, setActiveSection] =
useState<SettingsSection>(defaultSection);
useEffect(() => {
// When opening the dialog, ensure the active section follows the caller's intent.
// This allows triggers like "About" to open the dialog directly on that page.
if (dialogProps.open) {
setActiveSection(defaultSection);
}
}, [defaultSection, dialogProps.open]);
const sections = useMemo(
() => [
{
@@ -62,6 +71,7 @@ export function SettingsDialog(props: SettingsDialogProps) {
},
{ id: "tools", label: t.settings.sections.tools, icon: WrenchIcon },
{ id: "skills", label: t.settings.sections.skills, icon: SparklesIcon },
{ id: "about", label: t.settings.sections.about, icon: InfoIcon },
],
[
t.settings.sections.appearance,
@@ -69,6 +79,7 @@ export function SettingsDialog(props: SettingsDialogProps) {
t.settings.sections.tools,
t.settings.sections.skills,
t.settings.sections.notification,
t.settings.sections.about,
],
);
return (
@@ -122,7 +133,7 @@ export function SettingsDialog(props: SettingsDialogProps) {
/>
)}
{activeSection === "notification" && <NotificationSettingsPage />}
{activeSection === "acknowledge" && <AcknowledgePage />}
{activeSection === "about" && <AboutSettingsPage />}
</div>
</ScrollArea>
</div>

View File

@@ -32,11 +32,18 @@ import { SettingsDialog } from "./settings";
export function WorkspaceNavMenu() {
const [settingsOpen, setSettingsOpen] = useState(false);
const [settingsDefaultSection, setSettingsDefaultSection] = useState<
"appearance" | "memory" | "tools" | "skills" | "notification" | "about"
>("appearance");
const { open: isSidebarOpen } = useSidebar();
const { t } = useI18n();
return (
<>
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
<SettingsDialog
open={settingsOpen}
onOpenChange={setSettingsOpen}
defaultSection={settingsDefaultSection}
/>
<SidebarMenu className="w-full">
<SidebarMenuItem>
<DropdownMenu>
@@ -64,7 +71,12 @@ export function WorkspaceNavMenu() {
sideOffset={4}
>
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => setSettingsOpen(true)}>
<DropdownMenuItem
onClick={() => {
setSettingsDefaultSection("appearance");
setSettingsOpen(true);
}}
>
<Settings2Icon />
{t.common.settings}
</DropdownMenuItem>
@@ -108,7 +120,12 @@ export function WorkspaceNavMenu() {
</a>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSettingsDefaultSection("about");
setSettingsOpen(true);
}}
>
<InfoIcon />
{t.workspace.about}
</DropdownMenuItem>

View File

@@ -79,7 +79,12 @@ export const enUS: Translations = {
proMode: "Pro",
proModeDescription:
"Reasoning, planning and executing, get more accurate results, may take more time",
ultraMode: "Ultra",
ultraModeDescription:
"Pro mode with subagents enabled, maximum capability for complex multi-step tasks",
searchModels: "Search models...",
surpriseMe: "Surprise",
surpriseMePrompt: "Surprise me",
suggestions: [
{
suggestion: "Write",
@@ -214,7 +219,7 @@ export const enUS: Translations = {
tools: "Tools",
skills: "Skills",
notification: "Notification",
acknowledge: "Acknowledge",
about: "About",
},
memory: {
title: "Memory",

View File

@@ -62,7 +62,11 @@ export interface Translations {
reasoningModeDescription: string;
proMode: string;
proModeDescription: string;
ultraMode: string;
ultraModeDescription: string;
searchModels: string;
surpriseMe: string;
surpriseMePrompt: string;
suggestions: {
suggestion: string;
prompt: string;
@@ -161,7 +165,7 @@ export interface Translations {
tools: string;
skills: string;
notification: string;
acknowledge: string;
about: string;
};
memory: {
title: string;

View File

@@ -77,7 +77,11 @@ export const zhCN: Translations = {
reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡",
proMode: "专业",
proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间",
ultraMode: "超级",
ultraModeDescription: "专业模式加子代理,适用于复杂的多步骤任务,功能最强大",
searchModels: "搜索模型...",
surpriseMe: "小惊喜",
surpriseMePrompt: "给我一个小惊喜吧",
suggestions: [
{
suggestion: "写作",
@@ -209,7 +213,7 @@ export const zhCN: Translations = {
tools: "工具",
skills: "技能",
notification: "通知",
acknowledge: "致谢",
about: "关于",
},
memory: {
title: "记忆",

View File

@@ -21,9 +21,9 @@ export interface LocalSettings {
};
context: Omit<
AgentThreadContext,
"thread_id" | "is_plan_mode" | "thinking_enabled"
"thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled"
> & {
mode: "flash" | "thinking" | "pro" | undefined;
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
};
layout: {
sidebar_collapsed: boolean;

View File

@@ -0,0 +1,13 @@
import { createContext, useContext } from "react";
import type { SubagentState } from "../threads/types";
export const SubagentContext = createContext<Map<string, SubagentState>>(new Map());
export function useSubagentContext() {
const context = useContext(SubagentContext);
if (context === undefined) {
throw new Error("useSubagentContext must be used within a SubagentContext.Provider");
}
return context;
}

View File

@@ -0,0 +1,69 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { SubagentProgressEvent, SubagentState } from "../threads/types";
export function useSubagentStates() {
const [subagents, setSubagents] = useState<Map<string, SubagentState>>(new Map());
const subagentsRef = useRef<Map<string, SubagentState>>(new Map());
// 保持 ref 与 state 同步
useEffect(() => {
subagentsRef.current = subagents;
}, [subagents]);
const handleSubagentProgress = useCallback((event: SubagentProgressEvent) => {
console.log('[SubagentProgress] Received event:', event);
const { task_id, trace_id, subagent_type, event_type, result, error } = event;
setSubagents(prev => {
const newSubagents = new Map(prev);
const existingState = newSubagents.get(task_id) || {
task_id,
trace_id,
subagent_type,
status: "running" as const,
};
let newState = { ...existingState };
switch (event_type) {
case "started":
newState = {
...newState,
status: "running",
};
break;
case "completed":
newState = {
...newState,
status: "completed",
result,
};
break;
case "failed":
newState = {
...newState,
status: "failed",
error,
};
break;
}
newSubagents.set(task_id, newState);
return newSubagents;
});
}, []);
const clearSubagents = useCallback(() => {
setSubagents(new Map());
}, []);
return {
subagents,
handleSubagentProgress,
clearSubagents,
};
}

View File

@@ -0,0 +1,2 @@
export { useSubagentStates } from "./hooks";
export { SubagentContext, useSubagentContext } from "./context";

View File

@@ -135,6 +135,7 @@ export function useSubmitThread({
threadId: isNewThread ? threadId! : undefined,
streamSubgraphs: true,
streamResumable: true,
streamMode: ["values", "messages-tuple", "custom"],
config: {
recursion_limit: 1000,
},

View File

@@ -17,4 +17,5 @@ export interface AgentThreadContext extends Record<string, unknown> {
model_name: string | undefined;
thinking_enabled: boolean;
is_plan_mode: boolean;
subagent_enabled: boolean;
}

4
frontend/src/typings/md.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "*.md" {
const content: string;
export default content;
}