mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-24 22:54:46 +08:00
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:
100
frontend/AGENTS.md
Normal file
100
frontend/AGENTS.md
Normal 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
89
frontend/CLAUDE.md
Normal 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+.
|
||||
@@ -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;
|
||||
|
||||
@@ -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
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
@@ -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>
|
||||
@@ -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) {
|
||||
// 可以在这里添加保存功能
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)",
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 />
|
||||
|
||||
49
frontend/src/components/ui/confetti-button.tsx
Normal file
49
frontend/src/components/ui/confetti-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import about from "./about.md";
|
||||
|
||||
export function AboutSettingsPage() {
|
||||
return <Streamdown>{about}</Streamdown>;
|
||||
}
|
||||
52
frontend/src/components/workspace/settings/about.md
Normal file
52
frontend/src/components/workspace/settings/about.md
Normal 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.
|
||||
@@ -1,5 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export function AcknowledgePage() {
|
||||
return null;
|
||||
}
|
||||
@@ -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)}\``,
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: "记忆",
|
||||
|
||||
@@ -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;
|
||||
|
||||
13
frontend/src/core/subagents/context.ts
Normal file
13
frontend/src/core/subagents/context.ts
Normal 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;
|
||||
}
|
||||
69
frontend/src/core/subagents/hooks.ts
Normal file
69
frontend/src/core/subagents/hooks.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
2
frontend/src/core/subagents/index.ts
Normal file
2
frontend/src/core/subagents/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useSubagentStates } from "./hooks";
|
||||
export { SubagentContext, useSubagentContext } from "./context";
|
||||
@@ -135,6 +135,7 @@ export function useSubmitThread({
|
||||
threadId: isNewThread ? threadId! : undefined,
|
||||
streamSubgraphs: true,
|
||||
streamResumable: true,
|
||||
streamMode: ["values", "messages-tuple", "custom"],
|
||||
config: {
|
||||
recursion_limit: 1000,
|
||||
},
|
||||
|
||||
@@ -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
4
frontend/src/typings/md.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "*.md" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
Reference in New Issue
Block a user