Compare commits

..
25 Commits
Author SHA1 Message Date
董宏凯 aa1f81a30a ceshi1 2026-05-13 22:08:37 +08:00
fengxiangdebaozi 1c50c73aa4 样式修改 2026-05-13 21:41:13 +08:00
fengxiangdebaozi 6cc28c7edd Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server 2026-05-13 21:25:40 +08:00
fengxiangdebaozi 1c2cf768f8 样式修改 2026-05-13 21:25:29 +08:00
吉鑫升 481e718d86 Merge remote-tracking branch 'origin/master' 2026-05-13 21:02:27 +08:00
吉鑫升andClaude Opus 4.7 29f74afbf6 fix(scheduler): give every scheduled run its own throwaway thread
All scheduled tasks for a user shared one scheduler thread, so each run
appended its messages to the same LangGraph checkpoint. Over time the
history blew past the model's input window and every task on that
account started failing with context-too-long.

_execute_task now mints a fresh thread_id per run and threads it through
ensure_thread_meta / snapshot / start_run / extract_*. The per-task
scheduler_thread_id column is left in place for backwards compatibility
but no longer drives the LLM call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 20:58:23 +08:00
fengxiangdebaozi 02111ab5b8 文档修改 2026-05-13 20:22:14 +08:00
吉鑫升 a2f7157bc3 chore: remove .claude/settings.local.json from tracking
Followup to previous commit which only landed the .gitignore rule but
left the file still tracked in the index.
2026-05-13 20:14:37 +08:00
吉鑫升 e6591abfbc chore: stop tracking .claude/settings.local.json
This file holds per-user Claude Code permission allowlists and should not
be shared across the team. Adding it to .gitignore and removing from the
index; existing working copies are preserved.
2026-05-13 20:14:00 +08:00
吉鑫升 50bda1ddee Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji
# Conflicts:
#	frontend-web/src/components/workspace/agents/agent-card.tsx
#	frontend-web/src/pages/NewAgentPage.tsx
2026-05-13 20:10:21 +08:00
吉鑫升 31abad86d5 智能体配置 2026-05-13 20:04:06 +08:00
fengxiangdebaozi af7e90c87a Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server 2026-05-13 18:23:25 +08:00
fengxiangdebaozi 2e4bdf122e 页面结构修改 2026-05-13 18:18:05 +08:00
吉鑫升 83d7fa26fe Merge remote master docs 2026-05-13 17:28:04 +08:00
fengxiangdebaozi efc4d89fdf 文档 2026-05-13 17:17:46 +08:00
吉鑫升 a27206c543 Update local frontend and backend changes 2026-05-13 17:17:42 +08:00
吉鑫升 faca2c9a2d Merge branch 'likun' into master 2026-05-13 17:02:22 +08:00
fengxiangdebaozi 0cbd137b4f wendang1xiug1 2026-05-13 16:38:34 +08:00
likun cdf41d3e5e feat:完成技能化适配的意图识别功能 2026-05-13 16:01:00 +08:00
吉鑫升 aa034f8669 定时任务订阅模式 2026-05-13 14:59:27 +08:00
likun aee5a50b9f bug修复 2026-05-13 14:57:16 +08:00
likun b8926f00d7 新增用户意图确认功能 2026-05-13 14:36:44 +08:00
吉鑫升 839386f0b4 test: verify new remote push 2026-05-13 14:23:55 +08:00
吉鑫升 cf2e4bdd9c 智能体分权 2026-05-13 11:36:17 +08:00
吉鑫升 e7a455f22e Initial commit: deerflow-server (frontend-web + offline-backend-20260512) 2026-05-13 09:36:15 +08:00
598 changed files with 98158 additions and 93164 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
remote connectivity test - 2026-05-13
+65 -56
View File
@@ -1,56 +1,65 @@
# Workspace/editor
.vscode/
frontend-web/.vscode/
# Frontend local/runtime artifacts
frontend-web/.env
frontend-web/node_modules/
frontend-web/tsconfig.tsbuildinfo
frontend-web/vite-dev.log
frontend-web/vite-dev.err.log
# Frontend local packages/archives/docs (non-source)
frontend-web/pnpm-lock.yaml
frontend-web/README.md
frontend-web/doc.md
frontend-web/src.zip
frontend-web/src9.zip
frontend-web/src-v1.zip
frontend-web/src-*.rar
# Frontend build artifacts
frontend-web/dist-scheduled-check/
# Offline backend bundles and local deployment artifacts
offline-backend-20260505.zip
offline-backend-20260505/output/
offline-backend-20260505/bundle/.env
offline-backend-20260505/bundle/.env.example
offline-backend-20260505/bundle/DEPLOY_BACKEND_OFFLINE_ZH.md
offline-backend-20260505/bundle/SHA256SUMS
offline-backend-20260505/bundle/config.yaml
offline-backend-20260505/bundle/config.yaml.example
offline-backend-20260505/bundle/data/
offline-backend-20260505/bundle/deerflow-backend-offline-image-20260505.tar
offline-backend-20260505/bundle/extensions_config.json
offline-backend-20260505/bundle/frontend-vite.zip
offline-backend-20260505/bundle/skills/
offline-backend-20260505/bundle/start_backend.0509.sh
offline-backend-20260505/bundle/start_backend.sh
offline-backend-20260505/bundle/stop_backend.sh
# offline-backend-20260512 runtime artifacts (large files, local config, data)
offline-backend-20260512/.env
offline-backend-20260512/.env.example
offline-backend-20260512/DEPLOY_BACKEND_OFFLINE_ZH.md
offline-backend-20260512/SHA256SUMS
offline-backend-20260512/config.yaml
offline-backend-20260512/config.yaml.example
offline-backend-20260512/data/
offline-backend-20260512/deerflow-backend-offline-image-20260505.tar
offline-backend-20260512/extensions_config.json
offline-backend-20260512/skills/
offline-backend-20260512/wheelhouse/
offline-backend-20260512/start_backend.sh
offline-backend-20260512/stop_backend.sh
offline-backend-20260512/check_backend.sh
# Workspace/editor
.vscode/
frontend-web/.vscode/
# Claude Code local-only settings (per-user permissions, not shared)
.claude/settings.local.json
# Local runtime logs and pid files
.runtime/
# Frontend local/runtime artifacts
frontend-web/.env
frontend-web/node_modules/
frontend-web/.pnpm-store/
frontend-web/tsconfig.tsbuildinfo
frontend-web/vite-dev.log
frontend-web/vite-dev.err.log
# Frontend local packages/archives/docs (non-source)
frontend-web/pnpm-lock.yaml
frontend-web/README.md
frontend-web/doc.md
frontend-web/src.zip
frontend-web/src9.zip
frontend-web/src-v1.zip
frontend-web/src-*.rar
# Frontend build artifacts
frontend-web/dist-scheduled-check/
# Offline backend bundles and local deployment artifacts
offline-backend-20260505.zip
offline-backend-20260505/output/
offline-backend-20260505/bundle/.env
offline-backend-20260505/bundle/.env.example
offline-backend-20260505/bundle/DEPLOY_BACKEND_OFFLINE_ZH.md
offline-backend-20260505/bundle/SHA256SUMS
offline-backend-20260505/bundle/config.yaml
offline-backend-20260505/bundle/config.yaml.example
offline-backend-20260505/bundle/data/
offline-backend-20260505/bundle/deerflow-backend-offline-image-20260505.tar
offline-backend-20260505/bundle/extensions_config.json
offline-backend-20260505/bundle/frontend-vite.zip
offline-backend-20260505/bundle/skills/
offline-backend-20260505/bundle/start_backend.0509.sh
offline-backend-20260505/bundle/start_backend.sh
offline-backend-20260505/bundle/stop_backend.sh
# offline-backend-20260512 runtime artifacts (large files, local config, data)
offline-backend-20260512/.env
offline-backend-20260512/.env.example
offline-backend-20260512/DEPLOY_BACKEND_OFFLINE_ZH.md
offline-backend-20260512/SHA256SUMS
offline-backend-20260512/config.yaml
offline-backend-20260512/config.yaml.example
offline-backend-20260512/data/
offline-backend-20260512/deerflow-backend-offline-image-20260505.tar
offline-backend-20260512/extensions_config.json
offline-backend-20260512/skills/
offline-backend-20260512/wheelhouse/
offline-backend-20260512/backend/.deer-flow/data/*.db
offline-backend-20260512/backend/.deer-flow/data/*.db-*
offline-backend-20260512/start_backend.sh
offline-backend-20260512/stop_backend.sh
offline-backend-20260512/check_backend.sh
-10
View File
@@ -1,10 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/deerflow-server-master.iml" filepath="$PROJECT_DIR$/.idea/deerflow-server-master.iml" />
</modules>
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
-672
View File
@@ -1,672 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
DeerFlow is a full-stack AI agent system combining:
- **Frontend**: React/Vite web interface for interactive agent conversations
- **Backend**: LangGraph-based agent runtime with sandbox execution, persistent memory, and subagent delegation
This is a monorepo with frontend and backend components running in parallel.
---
## Architecture
```
┌────────────────────────────────────────────────────────┐
│ DeerFlow Application │
├────────────────────────────────────────────────────────┤
│ Nginx (Port 2026) │
│ (Unified reverse proxy) │
└─────────────────┬──────────────────┬──────────────────┘
│ │
/api/langgraph/* (Gateway) / (Frontend + /api/*)
│ │
┌──────▼──────────┐ ┌─────────▼────────────┐
│ Gateway API │ │ Frontend Web UI │
│ (Port 8001) │ │ (Port 3000) │
│ │ │ │
│ - LangGraph │ │ - React 19 + Vite │
│ - FastAPI │ │ - TypeScript │
│ - Tools │ │ - Tailwind CSS │
│ - Memory │ │ │
└─────────────────┘ └──────────────────────┘
│
┌───────▼──────────────────┐
│ Agent Runtime (LangGraph)│
│ ┌──────────────────────┐ │
│ │ Lead Agent │ │
│ │ ┌────────────────┐ │ │
│ │ │ Middlewares │ │ │
│ │ │ (10 total) │ │ │
│ │ └────────────────┘ │ │
│ │ ┌────────────────┐ │ │
│ │ │ Tools │ │ │
│ │ │ - Sandbox │ │ │
│ │ │ - MCP │ │ │
│ │ │ - Built-in │ │ │
│ │ │ - Subagents │ │ │
│ │ └────────────────┘ │ │
│ └──────────────────────┘ │
└───────────────────────────┘
```
**Request Routing**:
- `/api/langgraph/*` → Gateway (LangGraph runtime)
- `/api/*` (other) → Gateway (FastAPI REST endpoints)
- `/` → Frontend (React web interface)
---
## Project Structure
```
deerflow-server-master/
├── CLAUDE.md # This file (root guidance)
├── .gitignore # Git ignore rules
│
├── frontend-web/ # React/Vite web interface
│ ├── package.json # Frontend dependencies
│ ├── package-lock.json
│ ├── vite.config.ts # Vite bundler config
│ ├── tsconfig.json # TypeScript config
│ ├── tailwind.config.ts # Tailwind CSS config
│ ├── postcss.config.js # PostCSS config
│ ├── index.html # HTML entry point
│ └── src/
│ ├── App.tsx # Root component
│ ├── main.tsx # React DOM mount
│ ├── components/
│ │ ├── ai-elements/ # Agent UI components (message, artifact, plan, etc.)
│ │ ├── landing/ # Landing page components
│ │ ├── ui/ # Base UI components (shadcn/ui styled)
│ │ ├── workspace/ # Main workspace interface
│ │ ├── query-client-provider.tsx
│ │ └── theme-provider.tsx
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utilities (HTTP client, formatting)
│ ├── pages/ # Route pages
│ └── styles/ # Global CSS
│
└── offline-backend-20260512/backend/ # Python FastAPI + LangGraph backend
├── CLAUDE.md # Detailed backend guidance
├── README.md # Backend documentation
├── Makefile # Backend commands
├── pyproject.toml # Python dependencies + workspace
├── langgraph.json # LangGraph Studio configuration
│
├── packages/harness/ # deerflow-harness: Publishable agent framework
│ ├── pyproject.toml
│ └── deerflow/
│ ├── agents/ # LangGraph agent system
│ ├── sandbox/ # Code execution isolation
│ ├── subagents/ # Task delegation system
│ ├── tools/ # Tool ecosystem
│ ├── models/ # LLM model factory
│ ├── mcp/ # Model Context Protocol integration
│ ├── skills/ # Skills discovery & loading
│ ├── config/ # Configuration system
│ ├── community/ # Community tools (Tavily, Jina, Firecrawl)
│ ├── reflection/ # Dynamic module loading
│ └── client.py # Embedded DeerFlowClient
│
├── app/ # Unpublished application code
│ ├── gateway/ # FastAPI Gateway API
│ │ ├── app.py # FastAPI application
│ │ └── routers/ # REST endpoints
│ └── channels/ # IM integrations (Feishu, Slack, Telegram, DingTalk)
│
├── tests/ # Comprehensive test suite
├── docs/ # Detailed documentation
└── .deer-flow/ # Runtime directory (created at startup)
├── agents/ # Custom agent definitions
├── users/ # Per-user data
│ └── {user_id}/
│ ├── memory.json # Memory system
│ ├── threads/ # Per-thread data
│ └── agents/ # Per-agent memory
└── threads/ # Thread-specific workspace (legacy)
```
---
## Setup & Installation
### Prerequisites
- Node.js 18+ and pnpm
- Python 3.12+
- uv package manager
- API keys for LLM providers (OpenAI, Anthropic, etc.)
### Quick Start (Full Application)
```bash
cd offline-backend-20260512/backend
# Install all dependencies
make install
# Create config from template
cp config.example.yaml config.yaml
# Set your API keys
export OPENAI_API_KEY="your-key-here"
# ... other API keys as needed
# Start all services
make dev
```
Access at: **http://localhost:2026**
---
## Development Commands
### Full Application (from backend directory)
```bash
make install # Install frontend + backend dependencies
make dev # Start Gateway + Frontend + Nginx (port 2026)
make gateway # Run Gateway API only (port 8001)
make test # Run all tests
make lint # Lint code
make format # Format code with ruff
```
### Frontend Only (from frontend-web directory)
```bash
pnpm install # Install frontend dependencies
pnpm dev # Start Vite dev server (port 3000)
pnpm build # Build for production
pnpm preview # Preview production build
pnpm typecheck # TypeScript type checking
```
### Backend Only (from backend directory)
```bash
make install # Install dependencies
make dev # Run Gateway API with auto-reload (port 8001)
make gateway # Run Gateway API (no reload)
make test # Run tests
make lint # Check code style
make format # Auto-format code
```
---
## Architecture Highlights
### Frontend (`frontend-web/`)
- **Framework**: React 19 with TypeScript
- **Bundler**: Vite for fast dev/build
- **Styling**: Tailwind CSS + shadcn/ui components
- **State Management**: React Query (TanStack Query) + local state
- **Routing**: React Router v7
- **Key Libraries**:
- `@xyflow/react` - Node/edge visualization for agent workflows
- `codemirror` - Code editing with language support
- `sonner` - Toast notifications
- `motion` - Animation library
- `katex` - Math rendering
- `remark-gfm` + `rehype` - Markdown processing
**Main Components**:
- `components/ai-elements/` - Agent-specific UI (message, artifact, plan, canvas, etc.)
- `components/ui/` - Reusable shadcn/ui-style components
- `components/workspace/` - Main workspace interface
- `components/landing/` - Public landing page
### Backend (`offline-backend-20260512/backend/`)
The backend is split into two layers:
1. **Harness** (`packages/harness/deerflow/`)
- Publishable agent framework package
- Contains: agents, sandbox, tools, models, MCP, skills, config
- Import prefix: `from deerflow.*`
- Can be published as `deerflow-harness` PyPI package
2. **App** (`app/`)
- Unpublished application code
- Contains: FastAPI Gateway API, IM channel integrations
- Import prefix: `from app.*`
- **Boundary rule**: App imports Harness, but Harness never imports App
**Key Systems**:
- **Lead Agent** - Single LangGraph agent with 10 middleware components
- **Sandbox** - Per-thread isolated execution (local filesystem or Docker)
- **Subagents** - Background task delegation with 3-concurrent limit
- **Memory** - LLM-powered persistent context across conversations
- **Tools** - Sandbox (bash, read/write), MCP, community (web search), built-in (clarification, image view)
- **Skills** - Domain-specific workflows injected via system prompt
- **MCP** - Model Context Protocol server integration
**For detailed backend architecture, see `offline-backend-20260512/backend/CLAUDE.md`**
---
## Configuration
### Main Configuration (`config.yaml`)
Place in project root (same level as this file). See `offline-backend-20260512/backend/config.example.yaml` for full template.
Key sections:
- `models` - LLM configurations (OpenAI, Anthropic, etc.) with thinking/vision flags
- `tools` - Tool definitions with module paths
- `sandbox` - Execution environment provider
- `skills` - Skills directory paths
- `memory` - Memory system (enabled, storage, debounce)
- `title` - Auto-title generation
- `summarization` - Context summarization
- `subagents` - Task delegation (enabled/disabled)
Config values starting with `$` are resolved as environment variables (e.g., `$OPENAI_API_KEY`).
### Extensions Configuration (`extensions_config.json`)
Place in project root. Configures MCP servers and skill states:
```json
{
"mcpServers": {
"github": {
"enabled": true,
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {"GITHUB_TOKEN": "$GITHUB_TOKEN"}
}
},
"skills": {
"pdf-processing": {"enabled": true}
}
}
```
### Environment Variables
```bash
# LLM API Keys
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
# Tool API Keys
export TAVILY_API_KEY="tvly-..."
export GITHUB_TOKEN="ghp_..."
# Configuration paths
export DEER_FLOW_CONFIG_PATH="./config.yaml"
export DEER_FLOW_EXTENSIONS_CONFIG_PATH="./extensions_config.json"
# Tracing (optional)
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="lsv2_pt_..."
export LANGSMITH_PROJECT="myproject"
```
---
## Important Development Guidelines
### Documentation Updates
**CRITICAL**: Always update relevant documentation after code changes:
- Update `offline-backend-20260512/backend/README.md` for user-facing changes
- Update `offline-backend-20260512/backend/CLAUDE.md` for internal changes
- Keep documentation synchronized with code
### Testing
**Test-Driven Development (TDD) is mandatory** for backend:
```bash
cd offline-backend-20260512/backend
# Run all tests
make test
# Run specific test
PYTHONPATH=. uv run pytest tests/test_<feature>.py -v
```
Frontend testing is lighter-weight but should be done for critical components.
### Code Style
- **Backend**: `ruff` (line length: 240, Python 3.12+, double quotes)
- **Frontend**: TypeScript with eslint/prettier defaults
### Architecture Boundaries
The **Harness ↔ App boundary** (enforced by CI tests):
```python
# ✅ Allowed (App → Harness)
from deerflow.agents import make_lead_agent
# ❌ Forbidden (Harness → App)
from app.gateway.routers import ... # This will fail CI tests
```
---
## Key Features
### 1. Sandbox Execution
Per-thread isolated code execution with virtual path mapping:
- **Local**: Filesystem-based execution (default)
- **Docker**: Aio sandbox provider for complete isolation
- **Tools**: `bash`, `ls`, `read_file`, `write_file`, `str_replace`
- **Paths**: `/mnt/user-data/{workspace,uploads,outputs}` → thread-specific physical directories
### 2. Persistent Memory
LLM-powered context retention across conversations:
- Automatic extraction of facts and preferences
- Structured storage with confidence scoring
- Debounced updates to minimize LLM calls
- Injected into agent system prompt for context awareness
See `offline-backend-20260512/backend/docs/memory-improvements.md` for details.
### 3. Subagent Delegation
Background task execution with concurrent limits:
- Built-in agents: `general-purpose` (full toolset) and `bash` (commands)
- Max 3 concurrent subagents per turn
- 15-minute timeout per task
- Real-time status updates via SSE
### 4. Tool Ecosystem
Modular tool system supporting multiple providers:
- **Sandbox tools**: Code execution in isolated environments
- **Built-in tools**: `ask_clarification`, `view_image`, `present_files`
- **MCP tools**: Any Model Context Protocol server (stdio, SSE, HTTP)
- **Community tools**: Tavily (web search), Jina (web fetch), Firecrawl (scraping)
- **Skills**: Domain-specific workflows loaded from SKILL.md files
### 5. Model Context Protocol (MCP)
Integrate external tools and services:
- **Transports**: stdio, SSE, HTTP (with OAuth support)
- **Configuration**: `extensions_config.json` for server setup
- **Runtime updates**: Changes detected and reloaded automatically
### 6. Plan Mode
TodoList middleware for complex multi-step tasks:
- Controlled via runtime config: `is_plan_mode = True`
- Provides `write_todos` tool for task tracking
- Real-time task status updates
See `offline-backend-20260512/backend/docs/plan_mode_usage.md`.
### 7. Vision Support
For models with `supports_vision: true`:
- `ViewImageMiddleware` processes images automatically
- `view_image_tool` added to agent toolset
- Images converted to base64 and injected into state
### 8. File Upload
Multi-file upload with automatic document conversion:
- Supports: PDF, PPT, Excel, Word documents
- Auto-conversion via `markitdown`
- Endpoint: `POST /api/threads/{thread_id}/uploads`
- Files stored in thread-isolated directories
See `offline-backend-20260512/backend/docs/FILE_UPLOAD.md`.
### 9. IM Channel Integration
External messaging platform bridges:
- **Supported**: Feishu, Slack, Telegram, DingTalk
- **Architecture**: Channels use `langgraph-sdk` HTTP client
- **Streaming**: Feishu updates single card in place; Slack/Telegram use `wait()` response
- See `offline-backend-20260512/backend/app/channels/` for implementation
### 10. Context Summarization
Automatic conversation compression when approaching token limits:
- Configurable trigger types: tokens, messages, or token fraction
- Keeps recent messages while summarizing older ones
- See `offline-backend-20260512/backend/docs/summarization.md`.
---
## Middleware Chain (Backend)
The agent runtime executes 18 middleware components in strict order:
1. **ThreadDataMiddleware** - Per-thread isolated directories
2. **UploadsMiddleware** - Inject uploaded files
3. **SandboxMiddleware** - Acquire sandbox environment
4. **DanglingToolCallMiddleware** - Handle incomplete tool responses
5. **LLMErrorHandlingMiddleware** - Normalize provider errors
6. **GuardrailMiddleware** - Tool call authorization (optional)
7. **SandboxAuditMiddleware** - Security audit logging
8. **ToolErrorHandlingMiddleware** - Convert exceptions to error messages
9. **SummarizationMiddleware** - Context reduction (optional)
10. **TodoListMiddleware** - Task tracking (optional, plan mode)
11. **TokenUsageMiddleware** - Token tracking (optional)
12. **TitleMiddleware** - Auto-generate conversation titles
13. **MemoryMiddleware** - Queue for async memory updates
14. **ViewImageMiddleware** - Inject vision image data (conditional)
15. **DeferredToolFilterMiddleware** - Hide deferred tools (optional)
16. **SubagentLimitMiddleware** - Enforce concurrent subagent limit (optional)
17. **LoopDetectionMiddleware** - Detect and break tool-call loops
18. **ClarificationMiddleware** - Interrupt on clarification requests (must be last)
See `offline-backend-20260512/backend/CLAUDE.md` for detailed middleware descriptions.
---
## Common Workflows
### Adding a New Tool
1. Define tool in `packages/harness/deerflow/tools/` or `packages/harness/deerflow/community/`
2. Add to `config.yaml` `tools` section with module path
3. Write unit tests in `backend/tests/`
4. Update `CLAUDE.md` with tool description
5. Test via `make test`
### Creating a Custom Agent
1. Add agent metadata to database (`agents` table)
2. Create agent directory: `.deer-flow/agents/{agent_id}/SOUL.md`
3. Define agent behavior via SOUL.md frontmatter
4. Access via Gateway API: `GET /api/agents/{id}`
### Configuring MCP Servers
1. Add server to `extensions_config.json`:
```json
{
"mcpServers": {
"my-server": {
"enabled": true,
"type": "stdio",
"command": "npx",
"args": ["-y", "my-mcp-package"]
}
}
}
```
2. Restart Gateway API - MCP tools auto-loaded via lazy initialization
3. Tools available immediately in agent toolset
### Testing in Development
```bash
# Backend unit tests
cd offline-backend-20260512/backend
make test
# Run specific test
PYTHONPATH=. uv run pytest tests/test_memory_updater.py -v
# Frontend development
cd frontend-web
pnpm dev # Access at http://localhost:3000
```
---
## Port Mapping
| Service | Port | Purpose |
|---------|------|---------|
| **Frontend** | 3000 | React development server |
| **Gateway API** | 8001 | FastAPI REST endpoints + LangGraph runtime |
| **LangGraph Server** | 2024 | Raw LangGraph protocol (used internally) |
| **Nginx** | 2026 | Unified reverse proxy (public entry point) |
| **Provisioner** | 8002 | Kubernetes sandbox provisioner (optional, Docker dev only) |
---
## Important Files & References
### Backend Architecture
- `offline-backend-20260512/backend/CLAUDE.md` - Detailed backend guidance
- `offline-backend-20260512/backend/README.md` - User-facing backend documentation
- `offline-backend-20260512/backend/docs/ARCHITECTURE.md` - Deep-dive architecture
- `offline-backend-20260512/backend/docs/API.md` - Gateway API reference
- `offline-backend-20260512/backend/docs/CONFIGURATION.md` - Config schema
### Frontend Components
- `frontend-web/src/components/ai-elements/` - Agent UI components
- `frontend-web/src/components/ui/` - Reusable UI components
- `frontend-web/src/components/workspace/` - Main workspace
### Configuration
- `config.example.yaml` - Backend configuration template
- `extensions_config.json` - MCP servers and skills
- `offline-backend-20260512/backend/langgraph.json` - LangGraph Studio config
### Testing
- `offline-backend-20260512/backend/tests/` - Test suite
- Regression tests: `test_docker_sandbox_mode_detection.py`, `test_provisioner_kubeconfig.py`, `test_harness_boundary.py`
---
## Troubleshooting
### Port Already in Use
If a port is already in use, kill the process:
```bash
# Find process on port (e.g., 8001)
lsof -i :8001
# Kill process
kill -9 <PID>
```
### Configuration Issues
1. Verify `config.yaml` exists in project root
2. Run: `make config-upgrade` to auto-merge missing fields
3. Check `config_version` in `config.yaml` matches `config.example.yaml`
### Memory System Issues
Check memory state:
```bash
# List memory files
ls -la offline-backend-20260512/backend/.deer-flow/users/
# Reload memory
curl -X POST http://localhost:8001/api/memory/reload
```
### Test Failures
Ensure the test dependency boundary is not violated:
```bash
# Run boundary test specifically
cd offline-backend-20260512/backend
PYTHONPATH=. uv run pytest tests/test_harness_boundary.py -v
```
---
## Technology Stack
### Frontend
- **React** 19 - UI framework
- **TypeScript** - Type safety
- **Vite** - Fast bundler
- **Tailwind CSS** - Utility-first styling
- **React Router** - Client routing
- **TanStack Query** - Data fetching
- **XYFlow** - Graph visualization
- **CodeMirror** - Code editor
- **Shiki** - Syntax highlighting
- **Remark/Rehype** - Markdown processing
### Backend
- **LangGraph** 1.0.6+ - Agent orchestration framework
- **LangChain** 1.2.3+ - LLM abstractions and tools
- **FastAPI** 0.115.0+ - REST API framework
- **Uvicorn** - ASGI server
- **Python 3.12+** - Programming language
- **uv** - Fast Python package manager
- **Ruff** - Linting and formatting
- **Pytest** - Testing framework
- **langchain-mcp-adapters** - Model Context Protocol support
- **markitdown** - Document conversion
- **Tavily/Jina/Firecrawl** - Web tools
---
## Contributing
1. Create a feature branch from `master`
2. Make changes and write tests
3. Run linting and tests:
```bash
make lint
make test
```
4. Update relevant CLAUDE.md and README.md files
5. Create a pull request
### CI/CD
GitHub Actions runs:
- Backend unit tests (including harness boundary test)
- Linting checks
- Type checking
All must pass before merging to `master`.
---
## Support
For issues and feature requests:
- Check the detailed docs in `offline-backend-20260512/backend/docs/`
- Review existing tests for usage examples
- Ask Claude Code for help with `/help` command
---
## License & Attribution
See LICENSE file in project root.
For more details on specific systems, see:
- Backend: `offline-backend-20260512/backend/CLAUDE.md`
- Backend architecture: `offline-backend-20260512/backend/docs/ARCHITECTURE.md`
- Frontend: Components in `frontend-web/src/components/`
-4
View File
@@ -1,4 +0,0 @@
# 本地开发:直连 Gateway(与 offline-backend-20260512/backend 默认端口一致)
# 修改端口时请同步改这里,并重启 `npm run dev`
VITE_BACKEND_BASE_URL=http://127.0.0.1:8001/
VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api
-7
View File
@@ -1,7 +0,0 @@
# 复制为 .env 或 .env.development 后按需修改(Vite 仅暴露以 VITE_ 开头的变量)
#
# Gateway 根地址(REST:`/api/models`、`/api/threads/...` 等)
VITE_BACKEND_BASE_URL=http://127.0.0.1:8001
#
# LangGraph SDK 的 apiUrl:须指向带 `/api` 的前缀,以便请求落在 `/api/threads`、`/api/assistants` 等路由上
VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api
View File
Whitespace-only changes.
+140
View File
@@ -0,0 +1,140 @@
# App.tsx 路由与页面结构重构需求(首页与业务页分离)
## 一、需求背景
当前 `frontend-web/src/App.tsx` 同时承载了:
1. 登录/首页相关路由(如 `/login`、`/landing`)
2. 工作台业务相关路由(如 `/workspace/chats`、`/workspace/agents`、`/workspace/scheduled/*`)
3. 业务页所需布局与运行时包装(`WorkspaceLayout`、`ChatRuntime`)
这会导致 `App.tsx` 过重,职责不够单一,不利于后续维护和扩展。
---
## 二、目标(你提出的核心诉求)
你希望:
1. **除了登录与首页入口内容外**,将 `App.tsx` 中其余业务页面路由整体迁移到 `pages` 下一个新组件中。
2. 再创建一个“承接导航/入口”的页面(或路由容器),用于放置这些导航逻辑。
3. `App.tsx` 最终只保留顶层路由分发和基础 Provider,变得更薄。
> 你提到“`App.tsx 74-75 login`”,可理解为:保留登录/首页相关入口,其他工作台内容抽离。
---
## 三、重构范围
### 1)保留在 `App.tsx` 的内容
- 全局 Provider:`HashRouter`、`AppProviders`
- 登录/首页路由:
- `/`
- `/login`
- `/login/:username`
- `/landing`
- 404 路由(可留在 `App.tsx` 或交给新路由容器统一处理,二选一)
### 2)迁移出 `App.tsx` 的内容
将以下与工作台有关的路由从 `App.tsx` 抽离:
- `/workspace`(重定向)
- `/workspace/chats`
- `/workspace/chats/:thread_id`
- `/workspace/agents`
- `/workspace/agents/new`
- `/workspace/agents/:agent_id/chats`(重定向)
- `/workspace/agents/:agent_id/chats/:thread_id`
- `/workspace/scheduled`
- `/workspace/scheduled/tasks`
- `/workspace/scheduled/runs/:run_id`
以及配套的包装组件职责:
- `WorkspaceLayout`
- `ChatRuntime`
---
## 四、建议的新结构(方案)
## 方案 A(推荐):新增“业务路由容器组件”
在 `pages` 下新增一个路由容器组件,例如:
- `src/pages/WorkspaceRoutes.tsx`(或 `src/pages/AppWorkspaceRoutes.tsx`)
职责:
- 专门维护 `/workspace/*` 相关路由
- 内部统一使用 `WorkspaceLayout` 与 `ChatRuntime`
- 输出一个可被 `App.tsx` 挂载的路由节点
同时在 `App.tsx` 里只保留:
- 登录/首页/404
- 一个 `Route path="/workspace/*" element={<WorkspaceRoutes />} />`
---
## 五、“再建立一个页面放置导航”的落地解释
你说的“建立一个页面放置这些导航”,可以有两种理解:
1. **路由容器页(推荐)**:
- 不强调视觉 UI,重点是集中管理导航与路由跳转规则。
- 即上面的 `WorkspaceRoutes.tsx`。
2. **可见的导航页(可选)**:
- 例如新建 `WorkspaceHomePage.tsx`,展示聊天/Agents/定时任务入口按钮。
- 路由上把 `/workspace` 指向该页,而不是直接重定向到 `/workspace/chats/new`。
当前从你的描述看,优先建议第 1 种(先做结构整理),之后若有产品需求再做第 2 种可见导航页。
---
## 六、实施步骤(开发任务拆解)
1. 新建 `WorkspaceRoutes` 组件(`pages` 目录下)。
2. 把所有 `/workspace/*` 路由从 `App.tsx` 搬入该组件。
3. 把 `WorkspaceLayout` 与 `ChatRuntime` 移入该组件(或抽到共享 layout/runtime 文件)。
4. 在 `App.tsx` 中保留登录/首页路由,并挂载 `/workspace/*` 到新组件。
5. 校验以下访问路径行为一致:
- `/workspace` 重定向
- chats/agents/scheduled 各页面可正常进入
- 动态参数路由(`thread_id`、`agent_id`、`run_id`)正常
- 404 不受影响
6. 运行前端并做手工回归。
---
## 七、验收标准
1. `App.tsx` 明显瘦身,主要负责顶层入口路由。
2. `/workspace/*` 全部由新建组件统一管理。
3. 功能行为与改造前一致(或符合新定义)。
4. 代码可读性提升:登录入口与业务入口职责分离。
---
## 八、注意事项
1. 当前项目使用 `HashRouter`,路由写法需保持兼容。
2. `Navigate` 重定向逻辑要完整迁移,避免出现死循环或空白页。
3. `WorkspaceLayout` 与 `ChatRuntime` 的包裹层级不能错,否则可能影响侧边栏、输入区、子任务/Artifacts 状态。
4. 若后续再加模块(例如 `/workspace/settings`),优先在 `WorkspaceRoutes` 中扩展。
---
## 九、建议的命名(可直接采用)
- 新增组件文件:`src/pages/WorkspaceRoutes.tsx`
- (可选)可见导航页:`src/pages/WorkspaceHomePage.tsx`
---
## 十、后续可选优化
1. 将 `WorkspaceLayout` 抽到 `src/layouts/WorkspaceLayout.tsx`。
2. 将 `ChatRuntime` 抽到 `src/runtime/ChatRuntime.tsx`。
3. 把路由常量集中到 `src/core/router/paths.ts`,减少硬编码路径。
+975
View File
@@ -0,0 +1,975 @@
# DeerFlow 前端开发文档
## 目录
1. [整体目录结构](#1-整体目录结构)
2. [路由结构](#2-路由结构)
3. [核心数据流与状态管理](#3-核心数据流与状态管理)
4. [主要页面组件](#4-主要页面组件)
5. [核心业务组件](#5-核心业务组件)
6. [核心 Hooks 与工具函数](#6-核心-hooks-与工具函数)
7. [API 调用层](#7-api-调用层)
8. [国际化机制](#8-国际化机制)
9. [主题与样式体系](#9-主题与样式体系)
10. [关键业务流程](#10-关键业务流程)
11. [项目配置与依赖](#11-项目配置与依赖)
12. [常见开发模式](#12-常见开发模式)
---
## 1. 整体目录结构
```
frontend-web/src/
├── App.tsx # 应用入口,顶层路由 + Provider
├── env.ts # 环境变量类型定义
├── pages/ # 路由级页面组件
│ ├── PageRoutes.tsx # /page/* 根路由容器(含外部系统预留位)
│ ├── WorkspaceRoutes.tsx # /page/workspace/* 工作区路由
│ ├── LoginPage.tsx # 登录页
│ ├── LandingPage.tsx # 营销落地页(强制 dark 主题)
│ ├── ChatsPage.tsx # 对话列表
│ ├── ChatPage.tsx # 单条对话(核心页面)
│ ├── AgentsPage.tsx # 智能体库
│ ├── AgentChatPage.tsx # 与智能体对话
│ ├── NewAgentPage.tsx # 创建智能体(两步流程)
│ ├── ScheduledTasksPage.tsx # 定时任务管理
│ ├── ScheduledTaskRunDetailPage.tsx # 定时任务执行详情
│ ├── ScheduledChatPage.tsx # 定时任务对话跳转页
│ └── NotFoundPage.tsx # 404 页面
├── components/ # 业务组件库
│ ├── ui/ # shadcn/ui 基础组件(Button、Card、Dialog 等 40+)
│ ├── ai-elements/ # AI 交互可视化组件
│ │ ├── prompt-input/ # 输入框 Context + 子组件系统
│ │ ├── artifact.tsx # 制品展示
│ │ ├── canvas.tsx
│ │ ├── conversation.tsx # 虚拟滚动对话容器
│ │ ├── message.tsx
│ │ ├── model-selector.tsx
│ │ ├── code-block.tsx
│ │ └── shimmer.tsx
│ ├── workspace/ # 工作区业务组件(详见第 5 节)
│ └── landing/ # 落地页组件
├── core/ # 核心业务逻辑层(非 UI)
│ ├── api/ # API 客户端(LangGraph SDK + Fetch)
│ ├── auth/ # 认证管理
│ ├── config/ # 环境配置 & Base URL
│ ├── i18n/ # 国际化(上下文、翻译文件、语言检测)
│ ├── threads/ # 对话线程管理(最核心)
│ ├── agents/ # 智能体 CRUD
│ ├── models/ # 模型列表
│ ├── settings/ # 本地设置(LocalStorage 发布订阅)
│ ├── uploads/ # 文件上传
│ ├── artifacts/ # 制品管理
│ ├── memory/ # 用户记忆
│ ├── skills/ # 技能管理
│ ├── scheduled-tasks/ # 定时任务
│ ├── messages/ # 消息处理工具函数
│ ├── tasks/ # 子任务 Context
│ ├── notification/ # 系统通知
│ └── utils/ # 通用工具(文件下载、日期格式化等)
└── hooks/ # 全局自定义 hooks
```
---
## 2. 路由结构
> 项目使用 **HashRouter**,所有路由访问形如 `http://host/#/xxx`
### 2.1 App.tsx 顶层路由
| 路径 | 组件 | 说明 |
|------|------|------|
| `/` | Navigate | 重定向到 `/login` |
| `/login` | LoginPage | 登录(自动登录,无表单) |
| `/login/:username` | LoginPage | 带用户名登录 |
| `/landing` | LandingPage | 营销落地页(固定 dark 主题) |
| `/page/*` | PageRoutes | 应用内容根容器 |
| `*` | NotFoundPage | 404 |
### 2.2 PageRoutes.tsx(`/page/*`)
```
/page/
├── index → Navigate to "workspace"
├── workspace/* → WorkspaceRoutes
└── [TODO 预留] → 外部系统路由挂载位置
```
### 2.3 WorkspaceRoutes.tsx(`/page/workspace/*`)
| 路径 | 页面组件 | 布局包裹 |
|------|---------|--------|
| `index` | Navigate → `chats/new` | - |
| `chats` | ChatsPage | WorkspaceLayout |
| `chats/:thread_id` | ChatPage | WorkspaceLayout + ChatRuntime |
| `agents` | AgentsPage | WorkspaceLayout |
| `agents/new` | NewAgentPage | WorkspaceLayout |
| `agents/:agent_id/chats` | Navigate → `/page/workspace/agents` | - |
| `agents/:agent_id/chats/:thread_id` | AgentChatPage | WorkspaceLayout + ChatRuntime |
| `scheduled` | Navigate → `.../scheduled/tasks` | - |
| `scheduled/tasks` | ScheduledTasksPage | WorkspaceLayout |
| `scheduled/runs/:run_id` | ScheduledTaskRunDetailPage | WorkspaceLayout |
### 2.4 布局 Provider 层级结构
```
WorkspaceLayout
├── SidebarProvider(侧边栏展开/收起状态)
│ ├── WorkspaceSidebar
│ │ ├── WorkspaceHeader(Logo、新建按钮)
│ │ ├── WorkspaceNavChatList(Chats / Agents / Scheduled 导航)
│ │ ├── RecentChatList(最近会话列表)
│ │ └── WorkspaceNavMenu(Settings / About / GitHub)
│ ├── SidebarInset(主内容区)
│ │ └── [Page Content]
│ ├── CommandPalette(快捷命令面板)
│ └── Toaster(全局通知)
ChatRuntime(仅 ChatPage / AgentChatPage 使用)
├── SubtasksProvider(子任务执行状态)
├── ArtifactsProvider(制品选中/打开状态)
└── PromptInputProvider(输入框内容/附件状态)
```
---
## 3. 核心数据流与状态管理
### 3.1 全局 Provider 架构
```
App
└── HashRouter
└── AppProviders(useLocation 驱动主题判断)
├── NextThemesProvider
│ └── attribute="class",/landing 强制 forcedTheme="dark"
├── I18nProvider
│ └── initialLocale = detectLocale()(从 cookie 或浏览器语言)
└── QueryClientProvider(TanStack React Query 全局缓存)
```
### 3.2 全局 Context 列表
| Context | 文件 | 职责 | 消费 Hook |
|---------|------|------|-----------|
| `I18nContext` | `core/i18n/context.tsx` | 当前语言 + 翻译对象 | `useI18n()` |
| `SubtaskContext` | `core/tasks/context.tsx` | 实时子任务执行状态 | `useSubtaskContext()` |
| `ArtifactsContext` | `components/workspace/artifacts/context.tsx` | 制品选中/面板开关 | `useArtifacts()` |
| `PromptInputContext` | `components/ai-elements/prompt-input/` | 输入框内容、附件列表 | `usePromptInputController()` |
| `ThreadContext` | `components/workspace/messages/context.tsx` | 当前 thread 对象下传 | 直接读 context.thread |
### 3.3 LocalStorage 数据
**操作层:** `core/settings/store.ts`(发布-订阅,`useSyncExternalStore` 驱动响应式)
| Key | 内容 |
|-----|------|
| `deerflow.auth` | `{ access_token, token_type, user_id, email }` |
| `deerflow.local-settings` | `{ notification, context: { model_name, mode, reasoning_effort } }` |
| `deerflow.thread-model.{threadId}` | 每条线程的模型覆盖 |
### 3.4 SessionStorage 数据
| Key | 内容 |
|-----|------|
| `deerflow.run-id.{threadId}` | WebSocket run_id,用于断线重连 |
### 3.5 React Query 缓存 Key
| Query Key | 数据 |
|-----------|------|
| `["threads"]` | 线程列表 |
| `["thread", threadId]` | 单条线程 |
| `["agents"]` | 智能体列表 |
| `["models"]` | 模型列表 |
| `["skills"]` | 技能列表 |
| `["memory"]` | 用户记忆 |
| `["scheduled-tasks"]` | 定时任务列表 |
| `["scheduled-runs", taskId]` | 任务执行记录 |
---
## 4. 主要页面组件
### 4.1 ChatPage.tsx — 单条对话页面
**职责:** 管理单条聊天线程的完整交互,包括消息流、输入、工具调用展示、制品等。
**关键状态:**
```typescript
const [threadId, setThreadId] = useState(routeThreadId || uuid());
const [isNewThread, setIsNewThread] = useState(!routeThreadId || routeThreadId === "new");
const [hasStartedConversation, setHasStartedConversation] = useState(false);
const [showFollowups, setShowFollowups] = useState(false);
const [settings, setSettings] = useThreadSettings(threadId);
```
**核心 Hook:**
```typescript
const [thread, sendMessage, isUploading] = useThreadStream({
threadId: isNewThread ? undefined : threadId,
context: settings.context,
onStart: (createdThreadId) => {
setThreadId(createdThreadId);
setIsNewThread(false);
// 不刷新页面,直接更新 URL hash
history.replaceState(null, "", `...#/page/workspace/chats/${createdThreadId}`);
},
onFinish: (state) => {
// 后台通知
showNotification(state.title, { body: lastMessageText });
}
});
```
**主要子组件:**
| 组件 | 职责 |
|------|------|
| `<MessageList>` | 消息渲染(含工具调用、子任务、制品) |
| `<Welcome>` | 新对话欢迎屏 |
| `<InputBox>` | 输入框(模式选择、附件、发送) |
| `<ThreadTitle>` | 线程标题(自动生成后同步更新) |
| `<ExportTrigger>` | 导出对话按钮 |
| `<ArtifactTrigger>` | 制品面板开关 |
| `<TokenUsageIndicator>` | Token 用量展示(可选) |
| `<TodoList>` | 任务清单 |
| `<Suggestions>` | 追问建议 |
**数据流:**
```
用户输入 (InputBox)
↓ handleSubmit(message: PromptInputMessage)
↓ sendMessage(threadId, message)
↓ [内部] 上传文件 → 提交到 LangGraph API
↓ WebSocket 流返回消息
↓ thread.messages 更新 → MessageList 重渲染
```
---
### 4.2 AgentChatPage.tsx — 与智能体对话
与 ChatPage 结构基本一致,差异点:
- 从路由获取 `agent_id`,传入 `useThreadStream({ assistantId: agent_id })`
- Header 展示智能体名称 Badge
- "新建对话"跳转到 `/page/workspace/agents/${agent_id}/chats/new`
- URL 更新为 `#/page/workspace/agents/${agent_id}/chats/${threadId}`
---
### 4.3 NewAgentPage.tsx — 创建智能体(两步流程)
**Step 1:name(命名 + 选技能)**
```typescript
// 自动预选前 3 个启用的技能
useEffect(() => {
if (!isSkillSelectionTouched) {
setSelectedSkills(enabledSkills.slice(0, 3).map(s => s.name));
}
}, [enabledSkills]);
// 提交:验证名称 → 创建智能体
await checkAgentName(name); // 检查重名
await createAgent(request); // 创建,返回 agent.id
setStep("chat"); // 进入 Step 2
```
**Step 2:chat(初始化对话)**
```typescript
// 发送 bootstrap 消息触发智能体初始化
sendMessage(threadId, bootstrapMessage, { agent_id: agentId });
// 监听 setup_agent 工具完成事件
onToolEnd: (event) => {
if (event.name === "setup_agent") {
// 带重试轮询加载 agent 最新信息
retryLoadAgent(agentId);
}
}
```
---
### 4.4 ChatsPage.tsx — 对话列表
```typescript
const { data: threads } = useThreads({ limit: 50, sortBy: "updated_at" });
// 客户端过滤:排除定时任务线程 + 搜索关键词
const filteredThreads = useMemo(() =>
threads?.filter(t =>
t.metadata?.thread_type !== "scheduler" &&
titleOfThread(t).toLowerCase().includes(search)
), [threads, search]
);
```
---
### 4.5 LoginPage.tsx — 登录
```typescript
// 自动登录,无需用户操作
useEffect(() => {
loginByUsername(username || "guest")
.then(auth => {
setStoredAuth(auth);
router.replace("/page/workspace/chats/new");
})
.catch(err => setStatus("error"));
}, [username]);
```
---
### 4.6 ScheduledTasksPage.tsx — 定时任务
**功能:** 创建/编辑/发布/暂停/恢复定时任务,查看执行历史。
**Cron 表达式构建:**
```typescript
type ScheduleFreq = "hourly" | "daily" | "weekly" | "monthly";
function buildCron(config): string {
// hourly → "15 * * * *"
// daily → "0 9 * * *"
// weekly → "0 9 * * 1"
// monthly → "0 9 1 * *"
}
```
**核心操作:**
```typescript
const actions = useScheduledTaskActions();
actions.create.mutateAsync(payload);
actions.publish.mutateAsync(taskId);
actions.pause.mutateAsync(taskId);
actions.trigger.mutateAsync(taskId); // 立即执行一次
actions.subscribe.mutateAsync(params); // 订阅推送
```
---
## 5. 核心业务组件
### 5.1 消息系统(`components/workspace/messages/`)
| 文件 | 职责 |
|------|------|
| `message-list.tsx` | 消息列表容器,调用 `groupMessages` 分组后渲染 |
| `message-list-item.tsx` | 单条消息渲染(含 role 判断) |
| `message-group.tsx` | 消息组(human / assistant / processing 等) |
| `markdown-content.tsx` | Markdown 渲染(rehype 系列插件) |
| `subtask-card.tsx` | 子任务状态卡片 |
| `message-token-usage.tsx` | 单条消息 Token 用量 |
| `skeleton.tsx` | 流式加载骨架屏 |
| `context.tsx` | ThreadContext,向下传递 thread 对象 |
**消息分组类型(`groupMessages`):**
```typescript
type MessageGroup =
| "human" // 用户输入
| "assistant:processing" // 推理/工具调用中
| "assistant" // 最终响应
| "assistant:clarification" // 澄清请求
| "assistant:present-files" // 文件呈现
| "assistant:subagent"; // 子智能体消息
```
---
### 5.2 输入框(`components/workspace/input-box.tsx`)
**子组件层级:**
```
InputBox
└── PromptInput(Context Provider)
├── PromptInputBody
│ ├── PromptInputTextarea(文本输入)
│ ├── PromptInputAttachments(附件列表)
│ └── PromptInputTools(工具栏:附件上传、模式选择)
├── PromptInputFooter
│ ├── PromptInputSubmit(发送/停止按钮)
│ └── Suggestions(追问建议列表)
└── ModelSelector(模型下拉选择)
```
**四种会话模式:**
| Mode | 说明 |
|------|------|
| `flash` | 快速模式,无推理步骤 |
| `thinking` | 推理模式,开启深度思考 |
| `pro` | 专业模式,多模态 + 规划 |
| `ultra` | 超级模式,启用子智能体 |
**推理努力等级(mode ≠ flash 时可选):** `minimal` / `low` / `medium` / `high`
---
### 5.3 侧边栏(`components/workspace/workspace-sidebar.tsx`)
```
WorkspaceSidebar
├── SidebarHeader → WorkspaceHeader
│ ├── Logo(可点击,跳转 /page/workspace/chats/new)
│ └── 新建对话按钮
├── SidebarContent
│ ├── WorkspaceNavChatList
│ │ ├── 新建对话(/page/workspace/chats/new)
│ │ ├── 所有对话(/page/workspace/chats)
│ │ ├── 智能体(/page/workspace/agents)
│ │ └── 任务管理(/page/workspace/scheduled/tasks)
│ └── RecentChatList(最近会话,含搜索)
└── SidebarFooter → WorkspaceNavMenu
├── Settings(打开设置 Dialog)
├── About
└── GitHub
```
---
### 5.4 制品管理(`components/workspace/artifacts/`)
```typescript
interface ArtifactsContextType {
artifacts: string[]; // artifact ID 列表
selectedArtifact: string | null;
autoSelect: boolean; // 是否自动选中最新制品
open: boolean; // 面板是否展开
autoOpen: boolean;
select(artifact: string, autoSelect?: boolean): void;
deselect(): void;
setOpen(open: boolean): void;
}
```
**文件:**
| 文件 | 职责 |
|------|------|
| `context.tsx` | ArtifactsContext + Provider |
| `artifact-trigger.tsx` | 面板展开/收起按钮 |
| `artifact-file-list.tsx` | 制品文件列表 |
| `artifact-file-detail.tsx` | 单个制品详情(含代码编辑器) |
---
### 5.5 设置面板(`components/workspace/settings/`)
| 页面文件 | 内容 |
|---------|------|
| `appearance-settings-page.tsx` | 主题切换(light/dark/system)、语言切换 |
| `memory-settings-page.tsx` | 用户记忆(事实列表、总结管理) |
| `notification-settings-page.tsx` | 桌面通知开关 |
| `skill-settings-page.tsx` | 技能启用/禁用 |
| `tool-settings-page.tsx` | 工具配置 |
| `about-settings-page.tsx` | 版本号、仓库链接 |
---
### 5.6 命令面板(`components/workspace/command-palette.tsx`)
通过 `Cmd/Ctrl + K` 触发,提供快速导航和操作入口。
```typescript
const handleNewChat = useCallback(() => {
router.push("/page/workspace/chats/new");
setOpen(false);
}, [router]);
```
---
## 6. 核心 Hooks 与工具函数
### 6.1 useThreadStream(最核心 Hook)
**位置:** `core/threads/hooks.ts`
管理 LangGraph WebSocket 连接、消息流、乐观更新。
**签名:**
```typescript
const [thread, sendMessage, isUploading] = useThreadStream({
threadId?: string, // 有值则连接现有线程,无值表示新建
context: LocalSettings["context"], // 模型、模式等
isMock?: boolean,
assistantId?: string, // 默认 "lead_agent"
onStart?: (threadId: string) => void,
onFinish?: (state: AgentThreadState) => void,
onToolEnd?: (event: ToolEndEvent) => void,
});
```
**thread 对象关键属性:**
```typescript
thread: {
messages: Message[], // 含乐观消息的完整列表
isLoading: boolean, // 流是否进行中
isThreadLoading: boolean, // 初始线程数据是否加载中
values: AgentThreadState, // 线程最终状态
error?: Error,
stop: async () => void, // 终止当前流
}
```
**sendMessage 签名:**
```typescript
sendMessage(
threadId: string | undefined,
message: PromptInputMessage,
extraContext?: Partial<AgentThreadContext>,
options?: { isMock?: boolean }
): Promise<void>
```
**内部机制:**
- **乐观更新:** 用户发送后立即本地展示消息,不等服务端响应
- **文件上传:** 自动调用 `uploadFiles()` 后再提交
- **run_id 持久化:** sessionStorage 保存,支持页面刷新后断线重连
- **消息同步:** 流结束后通过 `queryClient.invalidateQueries(["threads"])` 刷新列表
---
### 6.2 useThreadSettings
**位置:** `core/settings/hooks.ts`
```typescript
const [settings, setSettings] = useThreadSettings(threadId);
// settings 结构
{
notification: { enabled: boolean },
context: {
model_name?: string,
mode: "flash" | "thinking" | "pro" | "ultra",
reasoning_effort?: "minimal" | "low" | "medium" | "high"
}
}
// 修改(合并更新)
setSettings("context", { mode: "pro" });
setSettings("notification", { enabled: true });
```
**工作原理:** `useSyncExternalStore` 监听 localStorage 变化,跨标签页同步。
---
### 6.3 useI18n
**位置:** `core/i18n/hooks.ts`
```typescript
const { t, locale, changeLocale } = useI18n();
t.sidebar.newChat // "New Chat"
t.agents.createPageTitle // "Create Agent"
t.toolCalls.moreSteps(3) // "Show 3 more steps"
changeLocale("zh-CN"); // 切换语言,自动存 cookie
```
---
### 6.4 useScheduledTaskActions
**位置:** `core/scheduled-tasks/hooks.ts`
```typescript
const actions = useScheduledTaskActions();
// 所有返回值均为 TanStack Query Mutation
actions.create // 创建定时任务
actions.update // 更新任务配置
actions.remove // 删除任务
actions.removeRun // 删除执行记录
actions.pause // 暂停
actions.resume // 恢复
actions.trigger // 立即触发一次
actions.publish // 发布(激活)
actions.unpublish // 取消发布
actions.subscribe // 订阅推送
actions.updateSubscription
actions.unsubscribe
```
---
### 6.5 工具函数
**消息处理(`core/messages/utils.ts`):**
```typescript
groupMessages(messages, mapper) // 按类型分组消息列表
extractTextFromMessage(message) // 提取纯文本
extractContentFromMessage(message) // 提取 Markdown/图像内容
extractReasoningContentFromMessage(msg) // 提取推理过程
hasToolCalls(message) // 是否含工具调用
hasPresentFiles(message) // 是否包含文件呈现
extractPresentFilesFromMessage(message) // 获取文件列表
isClarificationToolMessage(message) // 是否为澄清请求
```
**线程路由(`core/threads/utils.ts`):**
```typescript
pathOfThread(thread, context?)
// → "/page/workspace/agents/{agentRef}/chats/{threadId}"
// 或 "/page/workspace/chats/{threadId}"
titleOfThread(thread)
// → thread.values?.title ?? "Untitled"
textOfMessage(message)
// → 消息文本内容(string 或 content 数组中第一个 text)
```
**其他工具:**
```typescript
// core/utils/files.tsx
downloadFile(blob: Blob, filename: string)
// core/utils/datetime.ts
formatTimeAgo(dateString: string) // → "2 小时前" / "3 days ago"
```
---
## 7. API 调用层
### 7.1 API 客户端架构
**两层设计:**
**第一层:LangGraph SDK Client**(`core/api/api-client.ts`)
```typescript
const client = new LangGraphClient({
apiUrl: getLangGraphBaseURL(isMock),
onRequest(url, init) {
const headers = new Headers(init.headers ?? {});
headers.set("Authorization", getAuthorizationHeaderValue());
return { ...init, headers };
}
});
export function getAPIClient(isMock?: boolean): LangGraphClient
```
**第二层:Fetch Client**(`core/api/fetch-client.ts`)
```typescript
function apiFetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
return fetch(input, {
...init,
headers: withAuthHeaders(init?.headers)
});
}
```
### 7.2 主要 API 模块
| 模块 | 文件 | 关键函数 |
|------|------|---------|
| **Auth** | `core/auth/api.ts` | `loginByUsername(username)` → `LoginByUsernameResponse` |
| **Agents** | `core/agents/api.ts` | `createAgent()`, `updateAgent()`, `listAgents()`, `deleteAgent()`, `checkAgentName()` |
| **Models** | `core/models/api.ts` | `loadModels()` → `Model[]` |
| **Threads** | (via LangGraph SDK) | `useThreadStream()`, `useThreads()`, `useDeleteThread()`, `useRenameThread()` |
| **Skills** | `core/skills/api.ts` | `loadSkills()`, `enableSkill(name, enabled)` |
| **Uploads** | `core/uploads/api.ts` | `uploadFiles(threadId, files[])`, `listUploadedFiles()`, `deleteUploadedFile()` |
| **Memory** | `core/memory/api.ts` | `loadMemory()`, `createMemoryFact()`, `updateMemoryFact()`, `deleteMemoryFact()` |
| **Scheduled Tasks** | `core/scheduled-tasks/api.ts` | CRUD、publish、pause、resume、trigger、subscribe |
### 7.3 Base URL 配置
**文件:** `core/config/index.ts`
```typescript
export function getBackendBaseURL(): string {
// env.NEXT_PUBLIC_BACKEND_BASE_URL
// 默认:""(当前域名根路径)
}
export function getLangGraphBaseURL(isMock?: boolean): string {
// mock 模式:${origin}/mock/api
// 正常:${origin}/api/langgraph
// 或读取 env.NEXT_PUBLIC_LANGGRAPH_BASE_URL
}
```
### 7.4 认证流程
```
LoginPage 调用 loginByUsername(username)
↓ POST /api/auth/login
↓ 返回 { access_token, token_type, user_id, email }
↓ setStoredAuth() 存入 localStorage["deerflow.auth"]
↓
后续所有 API 请求
↓ getAuthorizationHeaderValue()
↓ → "Bearer {access_token}"
↓ 自动添加到 Authorization header
```
---
## 8. 国际化机制
### 8.1 文件结构
```
core/i18n/
├── context.tsx # I18nProvider(包裹 App)
├── hooks.ts # useI18n()
├── locale.ts # detectLocale(),Locale = "en-US" | "zh-CN"
├── cookies.ts # cookie 读写(持久化语言选择)
├── translations.ts # 翻译映射表入口
└── locales/
├── types.ts # Translations 接口定义(所有翻译 key 的类型)
├── en-US.ts # 英文翻译
└── zh-CN.ts # 简体中文翻译
```
### 8.2 Translations 接口分类
```typescript
interface Translations {
locale: { localName: string } // 语言自身名称
common: { ... } // 通用词汇(确认、取消等)
welcome: { ... } // 欢迎屏
inputBox: { ... } // 输入框(模式、占位符)
sidebar: { ... } // 侧边栏导航
agents: { ... } // 智能体相关
chats: { ... } // 对话相关
workspace: { ... } // 工作区通用
settings: { ... } // 设置面板
toolCalls: { ... } // 工具调用展示
uploads: { ... } // 文件上传
subtasks: { ... } // 子任务
tokenUsage: { ... } // Token 用量
shortcuts: { ... } // 快捷键说明
}
```
### 8.3 语言检测优先级
1. 读取 cookie `deerflow.locale`
2. 读取浏览器 `navigator.language`
3. 回退到 `en-US`
---
## 9. 主题与样式体系
### 9.1 主题系统
**库:** `next-themes`
```typescript
// App.tsx
<NextThemesProvider
attribute="class" // 通过 <html class="dark"> 应用主题
enableSystem // 跟随系统偏好
disableTransitionOnChange // 切换时禁用过渡动画
forcedTheme={pathname === "/landing" ? "dark" : undefined}
>
```
**可选主题值:** `light` / `dark` / `system`
### 9.2 样式方案
**基础:** Tailwind CSS
**组件库:** shadcn/ui(`components/ui/` 目录,40+ 基础组件)
**主要 CSS 自定义属性:**
```css
--container-width-sm /* 小容器宽度 */
--container-width-md /* 中等容器宽度(对话主区域) */
--primary /* 主色 */
--background /* 页面背景 */
--foreground /* 主文本色 */
--muted-foreground /* 次要文本色 */
--border /* 边框色 */
--sidebar-foreground /* 侧边栏文本 */
```
### 9.3 样式使用规范
- 工具类优先(Tailwind)
- 动态样式用 `cn()` 工具函数合并(基于 `clsx` + `tailwind-merge`)
- 主题感知样式使用 CSS 变量(如 `text-muted-foreground`)
---
## 10. 关键业务流程
### 10.1 新建对话流程
```
1. 点击"新建对话" → navigate("/page/workspace/chats/new")
2. ChatPage 挂载,threadId = uuid(),isNewThread = true
3. 用户输入消息,点击发送
4. sendMessage(undefined, message) 调用
5. useThreadStream 内部:
a. 上传文件(若有)
b. 创建 LangGraph 线程,后端返回 threadId
c. onStart(threadId) 回调:
- setIsNewThread(false)
- history.replaceState 更新 URL hash
6. WebSocket 流开始接收 AI 消息
7. MessageList 实时渲染消息
8. 流结束,onFinish 回调,可能触发桌面通知
```
### 10.2 创建智能体流程
```
Step 1(name 步骤)
↓ 用户输入名称
↓ checkAgentName(name) → 验证是否重名
↓ createAgent({ name, skills: selectedSkills })
↓ 后端返回 agent.id,setStep("chat")
Step 2(chat 步骤)
↓ 生成临时 threadId
↓ useThreadStream({ assistantId: agent.id })
↓ 发送 bootstrap 消息(触发智能体自我设置)
↓ onToolEnd 监听:event.name === "setup_agent"
↓ 指数退避重试加载 agent 信息,确认已保存
↓ 显示"Agent Created"完成界面
↓ 用户可点击"开始对话"或"返回列表"
```
### 10.3 文件上传流程
```
1. 用户在 InputBox 选择/粘贴文件
2. PromptInputAttachments 展示预览列表
3. 用户点击发送
4. sendMessage 内部调用 uploadFiles(threadId, files)
5. POST /api/threads/{threadId}/upload
6. 后端返回 { files: [{ filename, size, virtual_path }] }
7. 构建 Message additional_kwargs.files
8. 完整 Message 提交到 LangGraph
```
---
## 11. 项目配置与依赖
### 11.1 主要依赖
| 包 | 版本 | 用途 |
|----|------|------|
| `react` | ^18 | 核心框架 |
| `react-router-dom` | ^6 | 客户端路由(HashRouter) |
| `@tanstack/react-query` | - | 服务端状态缓存 |
| `@langchain/langgraph-sdk` | - | LangGraph WebSocket 连接 |
| `next-themes` | - | 主题管理 |
| `sonner` | - | Toast 通知 |
| `lucide-react` | - | 图标库 |
| `tailwindcss` | - | CSS 框架 |
| `rehype` 系列 | - | Markdown 渲染 |
| `uuid` | - | 生成临时 thread ID |
### 11.2 环境变量
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `NEXT_PUBLIC_BACKEND_BASE_URL` | 后端接口基础 URL | `""` (当前域名) |
| `NEXT_PUBLIC_LANGGRAPH_BASE_URL` | LangGraph API URL | `/api/langgraph` |
| `NEXT_PUBLIC_STATIC_WEBSITE_ONLY` | 纯静态演示模式 | `"false"` |
---
## 12. 常见开发模式
### 12.1 添加新工作区页面
```
1. 创建 src/pages/MyPage.tsx
2. 在 WorkspaceRoutes.tsx 中添加路由:
<Route path="my-page" element={<WorkspaceLayout><MyPage /></WorkspaceLayout>} />
3. 在 workspace-nav-chat-list.tsx 中添加导航入口(如需要)
4. 更新相关的跳转路径
```
### 12.2 添加新 API 接口
```
1. 在 core/xxx/api.ts 中定义 API 函数(使用 apiFetch 或 getAPIClient())
2. 在 core/xxx/hooks.ts 中封装 useQuery / useMutation
3. 在页面或组件中调用 hook
```
### 12.3 添加新 Context
```
1. 创建 core/xxx/context.tsx:定义 Context + Provider + 默认值
2. 创建消费 hook(如 useXxx())
3. 在合适的组件树根部插入 Provider(WorkspaceRoutes 或 ChatRuntime)
```
### 12.4 添加国际化文本
```
1. 在 core/i18n/locales/types.ts 的 Translations 接口中添加字段
2. 在 core/i18n/locales/en-US.ts 添加英文内容
3. 在 core/i18n/locales/zh-CN.ts 添加中文内容
4. 组件中通过 useI18n() 访问:const { t } = useI18n(); t.xxx.yyy
```
### 12.5 引入外部系统路由
在 `src/pages/PageRoutes.tsx` 中找到 TODO 注释位置,填入:
```typescript
import AnotherSystemRoutes from "外部系统路径";
// 在 Routes 中添加:
<Route path="other/*" element={<AnotherSystemRoutes />} />
```
如外部系统有独立 Provider 依赖,在此文件或 App.tsx 中包裹。
---
## 附:关键文件速查
| 需求 | 关键文件 |
|------|---------|
| 修改路由结构 | `App.tsx`, `PageRoutes.tsx`, `WorkspaceRoutes.tsx` |
| 修改侧边栏导航 | `components/workspace/workspace-nav-chat-list.tsx` |
| 修改对话逻辑 | `pages/ChatPage.tsx`, `core/threads/hooks.ts` |
| 修改输入框 | `components/workspace/input-box.tsx`, `components/ai-elements/prompt-input/` |
| 修改消息渲染 | `components/workspace/messages/` |
| 添加 API 接口 | `core/xxx/api.ts` + `core/xxx/hooks.ts` |
| 修改全局设置 | `core/settings/store.ts`, `core/settings/hooks.ts` |
| 修改翻译文本 | `core/i18n/locales/en-US.ts`, `core/i18n/locales/zh-CN.ts` |
| 修改主题 | `App.tsx` (NextThemesProvider) |
| 修改认证逻辑 | `core/auth/api.ts`, `pages/LoginPage.tsx` |
| 定时任务 | `pages/ScheduledTasksPage.tsx`, `core/scheduled-tasks/` |
| 智能体管理 | `pages/NewAgentPage.tsx`, `core/agents/` |
+12 -12
View File
@@ -1,12 +1,12 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DeerFlow Vite</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CM 助手</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+8920 -8920
View File
File diff suppressed because it is too large. Load diff
+96 -95
View File
@@ -1,95 +1,96 @@
{
"name": "deer-flow-frontend-vite",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/language-data": "^6.5.2",
"@langchain/core": "^1.1.15",
"@langchain/langgraph-sdk": "^1.5.3",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@tanstack/react-query": "^5.90.17",
"@types/hast": "^3.0.4",
"@uiw/codemirror-theme-basic": "^4.25.4",
"@uiw/codemirror-theme-monokai": "^4.25.4",
"@uiw/react-codemirror": "^4.25.4",
"@xyflow/react": "^12.10.0",
"ai": "^6.0.33",
"best-effort-json-parser": "^1.2.1",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"codemirror": "^6.0.2",
"date-fns": "^4.1.0",
"dotenv": "^17.2.3",
"embla-carousel-react": "^8.6.0",
"gsap": "^3.13.0",
"hast": "^1.0.0",
"katex": "^0.16.28",
"lucide-react": "^0.562.0",
"motion": "^12.26.2",
"nanoid": "^5.1.6",
"next-themes": "^0.4.6",
"ogl": "^1.0.11",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-resizable-panels": "^4.4.1",
"react-router-dom": "^7.9.4",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"shiki": "3.15.0",
"sonner": "^2.0.7",
"streamdown": "1.4.0",
"tailwind-merge": "^3.4.0",
"tokenlens": "^1.3.1",
"unist-util-visit": "^5.0.0",
"use-stick-to-bottom": "^1.1.1",
"uuid": "^14.0.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.15",
"@types/gsap": "^3.0.0",
"@types/node": "^20.14.10",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.4",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.15",
"tw-animate-css": "^1.4.0",
"typescript": "^5.8.2",
"vite": "^6.0.0"
},
"packageManager": "pnpm@10.26.2"
}
{
"name": "deer-flow-frontend-vite",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/language-data": "^6.5.2",
"@langchain/core": "^1.1.15",
"@langchain/langgraph-sdk": "^1.5.3",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@tanstack/react-query": "^5.90.17",
"@types/hast": "^3.0.4",
"@uiw/codemirror-theme-basic": "^4.25.4",
"@uiw/codemirror-theme-monokai": "^4.25.4",
"@uiw/react-codemirror": "^4.25.4",
"@xyflow/react": "^12.10.0",
"ai": "^6.0.33",
"best-effort-json-parser": "^1.2.1",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"codemirror": "^6.0.2",
"date-fns": "^4.1.0",
"docx": "^9.5.1",
"dotenv": "^17.2.3",
"embla-carousel-react": "^8.6.0",
"gsap": "^3.13.0",
"hast": "^1.0.0",
"katex": "^0.16.28",
"lucide-react": "^0.562.0",
"motion": "^12.26.2",
"nanoid": "^5.1.6",
"next-themes": "^0.4.6",
"ogl": "^1.0.11",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-resizable-panels": "^4.4.1",
"react-router-dom": "^7.9.4",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"shiki": "3.15.0",
"sonner": "^2.0.7",
"streamdown": "1.4.0",
"tailwind-merge": "^3.4.0",
"tokenlens": "^1.3.1",
"unist-util-visit": "^5.0.0",
"use-stick-to-bottom": "^1.1.1",
"uuid": "^14.0.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.15",
"@types/gsap": "^3.0.0",
"@types/node": "^20.14.10",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.4",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.15",
"tw-animate-css": "^1.4.0",
"typescript": "^5.8.2",
"vite": "^6.0.0"
},
"packageManager": "pnpm@10.26.2"
}
+5 -5
View File
@@ -1,5 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
+2 -107
View File
@@ -3,27 +3,14 @@ import { Navigate, Route, Routes } from "react-router-dom";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { QueryClientProvider } from "@/components/query-client-provider";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { PromptInputProvider } from "@/components/ai-elements/prompt-input";
import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
import { CommandPalette } from "@/components/workspace/command-palette";
import { Toaster } from "sonner";
import { I18nProvider } from "@/core/i18n/context";
import { detectLocale } from "@/core/i18n/locale";
import { SubtasksProvider } from "@/core/tasks/context";
import { ArtifactsProvider } from "@/components/workspace/artifacts";
import { HashRouter, useLocation } from "react-router-dom";
import LandingPage from "./pages/LandingPage";
import ChatsPage from "./pages/ChatsPage";
import ChatPage from "./pages/ChatPage";
import AgentsPage from "./pages/AgentsPage";
import AgentChatPage from "./pages/AgentChatPage";
import NewAgentPage from "./pages/NewAgentPage";
import LoginPage from "./pages/LoginPage";
import ScheduledTaskRunDetailPage from "./pages/ScheduledTaskRunDetailPage";
import ScheduledTasksPage from "./pages/ScheduledTasksPage";
import PageRoutes from "./pages/PageRoutes";
import { NotFoundPage } from "./pages/NotFoundPage";
function AppProviders({ children }: { children: React.ReactNode }) {
@@ -45,27 +32,6 @@ function AppProviders({ children }: { children: React.ReactNode }) {
);
}
function WorkspaceLayout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider className="h-screen" defaultOpen>
<WorkspaceSidebar />
<SidebarInset className="min-w-0">{children}</SidebarInset>
<CommandPalette />
<Toaster position="top-center" />
</SidebarProvider>
);
}
function ChatRuntime({ children }: { children: React.ReactNode }) {
return (
<SubtasksProvider>
<ArtifactsProvider>
<PromptInputProvider>{children}</PromptInputProvider>
</ArtifactsProvider>
</SubtasksProvider>
);
}
export function App() {
return (
<HashRouter>
@@ -75,78 +41,7 @@ export function App() {
<Route path="/login" element={<LoginPage />} />
<Route path="/login/:username" element={<LoginPage />} />
<Route path="/landing" element={<LandingPage />} />
<Route
path="/workspace"
element={<Navigate to="/workspace/chats/new" replace />}
/>
<Route
path="/workspace/chats"
element={
<WorkspaceLayout>
<ChatsPage />
</WorkspaceLayout>
}
/>
<Route
path="/workspace/chats/:thread_id"
element={
<WorkspaceLayout>
<ChatRuntime>
<ChatPage />
</ChatRuntime>
</WorkspaceLayout>
}
/>
<Route
path="/workspace/agents"
element={
<WorkspaceLayout>
<AgentsPage />
</WorkspaceLayout>
}
/>
<Route
path="/workspace/scheduled"
element={<Navigate to="/workspace/scheduled/tasks" replace />}
/>
<Route
path="/workspace/scheduled/tasks"
element={
<WorkspaceLayout>
<ScheduledTasksPage />
</WorkspaceLayout>
}
/>
<Route
path="/workspace/scheduled/runs/:run_id"
element={
<WorkspaceLayout>
<ScheduledTaskRunDetailPage />
</WorkspaceLayout>
}
/>
<Route
path="/workspace/agents/new"
element={
<WorkspaceLayout>
<NewAgentPage />
</WorkspaceLayout>
}
/>
<Route
path="/workspace/agents/:agent_id/chats"
element={<Navigate to="/workspace/agents" replace />}
/>
<Route
path="/workspace/agents/:agent_id/chats/:thread_id"
element={
<WorkspaceLayout>
<ChatRuntime>
<AgentChatPage />
</ChatRuntime>
</WorkspaceLayout>
}
/>
<Route path="/page/*" element={<PageRoutes />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</AppProviders>
@@ -1,158 +1,158 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { type LucideIcon, XIcon } from "lucide-react";
import { forwardRef, type ComponentProps, type HTMLAttributes } from "react";
export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
export const Artifact = ({ className, ...props }: ArtifactProps) => (
<div
className={cn(
"flex flex-col overflow-hidden rounded-[1.35rem] border border-[#ece7dd] bg-white shadow-[0_24px_60px_-28px_rgba(15,23,42,0.16)] ring-1 ring-black/3",
className,
)}
{...props}
/>
);
export type ArtifactHeaderProps = HTMLAttributes<HTMLDivElement>;
export const ArtifactHeader = ({
className,
...props
}: ArtifactHeaderProps) => (
<div
className={cn(
"flex items-center justify-between border-b border-[#efe9de] bg-white px-4 py-3",
className,
)}
{...props}
/>
);
export type ArtifactCloseProps = ComponentProps<typeof Button>;
export const ArtifactClose = ({
className,
children,
size = "sm",
variant = "ghost",
...props
}: ArtifactCloseProps) => (
<Button
className={cn(
"text-muted-foreground hover:text-foreground size-8 p-0",
className,
)}
size={size}
type="button"
variant={variant}
{...props}
>
{children ?? <XIcon className="size-4" />}
<span className="sr-only">Close</span>
</Button>
);
export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>;
export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
<div
className={cn(
"text-foreground text-sm font-semibold tracking-tight",
className,
)}
{...props}
/>
);
export type ArtifactDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
export const ArtifactDescription = ({
className,
...props
}: ArtifactDescriptionProps) => (
<p className={cn("text-muted-foreground text-sm", className)} {...props} />
);
export type ArtifactActionsProps = HTMLAttributes<HTMLDivElement>;
export const ArtifactActions = ({
className,
...props
}: ArtifactActionsProps) => (
<div className={cn("flex items-center gap-1.5", className)} {...props} />
);
export type ArtifactActionProps = ComponentProps<typeof Button> & {
tooltip?: string;
label?: string;
icon?: LucideIcon;
};
export const ArtifactAction = ({
tooltip,
label,
icon: Icon,
children,
className,
size = "sm",
variant = "ghost",
...props
}: ArtifactActionProps) => {
const button = (
<Button
className={cn(
"text-muted-foreground hover:text-foreground hover:bg-accent/70 size-8 rounded-full p-0 transition-all duration-200 hover:scale-[1.03]",
className,
)}
size={size}
type="button"
variant={variant}
{...props}
>
{Icon ? <Icon className="size-4" /> : children}
<span className="sr-only">{label || tooltip}</span>
</Button>
);
if (tooltip) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return button;
};
export type ArtifactContentProps = HTMLAttributes<HTMLDivElement>;
export const ArtifactContent = forwardRef<HTMLDivElement, ArtifactContentProps>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"min-h-0 flex-1 overflow-auto bg-white p-4",
className,
)}
{...props}
/>
),
);
ArtifactContent.displayName = "ArtifactContent";
"use client";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { type LucideIcon, XIcon } from "lucide-react";
import { forwardRef, type ComponentProps, type HTMLAttributes } from "react";
export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
export const Artifact = ({ className, ...props }: ArtifactProps) => (
<div
className={cn(
"flex flex-col overflow-hidden rounded-[1.35rem] border border-[#ece7dd] bg-white shadow-[0_24px_60px_-28px_rgba(15,23,42,0.16)] ring-1 ring-black/3",
className,
)}
{...props}
/>
);
export type ArtifactHeaderProps = HTMLAttributes<HTMLDivElement>;
export const ArtifactHeader = ({
className,
...props
}: ArtifactHeaderProps) => (
<div
className={cn(
"flex items-center justify-between border-b border-[#efe9de] bg-white px-4 py-3",
className,
)}
{...props}
/>
);
export type ArtifactCloseProps = ComponentProps<typeof Button>;
export const ArtifactClose = ({
className,
children,
size = "sm",
variant = "ghost",
...props
}: ArtifactCloseProps) => (
<Button
className={cn(
"text-muted-foreground hover:text-foreground size-8 p-0",
className,
)}
size={size}
type="button"
variant={variant}
{...props}
>
{children ?? <XIcon className="size-4" />}
<span className="sr-only">Close</span>
</Button>
);
export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>;
export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
<div
className={cn(
"text-foreground text-sm font-semibold tracking-tight",
className,
)}
{...props}
/>
);
export type ArtifactDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
export const ArtifactDescription = ({
className,
...props
}: ArtifactDescriptionProps) => (
<p className={cn("text-muted-foreground text-sm", className)} {...props} />
);
export type ArtifactActionsProps = HTMLAttributes<HTMLDivElement>;
export const ArtifactActions = ({
className,
...props
}: ArtifactActionsProps) => (
<div className={cn("flex items-center gap-1.5", className)} {...props} />
);
export type ArtifactActionProps = ComponentProps<typeof Button> & {
tooltip?: string;
label?: string;
icon?: LucideIcon;
};
export const ArtifactAction = ({
tooltip,
label,
icon: Icon,
children,
className,
size = "sm",
variant = "ghost",
...props
}: ArtifactActionProps) => {
const button = (
<Button
className={cn(
"text-muted-foreground hover:text-foreground hover:bg-accent/70 size-8 rounded-full p-0 transition-all duration-200 hover:scale-[1.03]",
className,
)}
size={size}
type="button"
variant={variant}
{...props}
>
{Icon ? <Icon className="size-4" /> : children}
<span className="sr-only">{label || tooltip}</span>
</Button>
);
if (tooltip) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return button;
};
export type ArtifactContentProps = HTMLAttributes<HTMLDivElement>;
export const ArtifactContent = forwardRef<HTMLDivElement, ArtifactContentProps>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"min-h-0 flex-1 overflow-auto bg-white p-4",
className,
)}
{...props}
/>
),
);
ArtifactContent.displayName = "ArtifactContent";
@@ -1,22 +1,22 @@
import { Background, ReactFlow, type ReactFlowProps } from "@xyflow/react";
import type { ReactNode } from "react";
import "@xyflow/react/dist/style.css";
type CanvasProps = ReactFlowProps & {
children?: ReactNode;
};
export const Canvas = ({ children, ...props }: CanvasProps) => (
<ReactFlow
deleteKeyCode={["Backspace", "Delete"]}
fitView
panOnDrag={false}
panOnScroll
selectionOnDrag={true}
zoomOnDoubleClick={false}
{...props}
>
<Background bgColor="var(--sidebar)" />
{children}
</ReactFlow>
);
import { Background, ReactFlow, type ReactFlowProps } from "@xyflow/react";
import type { ReactNode } from "react";
import "@xyflow/react/dist/style.css";
type CanvasProps = ReactFlowProps & {
children?: ReactNode;
};
export const Canvas = ({ children, ...props }: CanvasProps) => (
<ReactFlow
deleteKeyCode={["Backspace", "Delete"]}
fitView
panOnDrag={false}
panOnScroll
selectionOnDrag={true}
zoomOnDoubleClick={false}
{...props}
>
<Background bgColor="var(--sidebar)" />
{children}
</ReactFlow>
);
@@ -1,239 +1,239 @@
"use client";
import { useControllableState } from "@radix-ui/react-use-controllable-state";
import { Badge } from "@/components/ui/badge";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import {
BrainIcon,
ChevronDownIcon,
DotIcon,
type LucideIcon,
} from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import {
createContext,
isValidElement,
memo,
useContext,
useMemo,
} from "react";
type ChainOfThoughtContextValue = {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
};
const ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>(
null,
);
const useChainOfThought = () => {
const context = useContext(ChainOfThoughtContext);
if (!context) {
throw new Error(
"ChainOfThought components must be used within ChainOfThought",
);
}
return context;
};
export type ChainOfThoughtProps = ComponentProps<"div"> & {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
};
export const ChainOfThought = memo(
({
className,
open,
defaultOpen = false,
onOpenChange,
children,
...props
}: ChainOfThoughtProps) => {
const [isOpen, setIsOpen] = useControllableState({
prop: open,
defaultProp: defaultOpen,
onChange: onOpenChange,
});
const chainOfThoughtContext = useMemo(
() => ({ isOpen, setIsOpen }),
[isOpen, setIsOpen],
);
return (
<ChainOfThoughtContext.Provider value={chainOfThoughtContext}>
<div className={cn("not-prose", className)} {...props}>
{children}
</div>
</ChainOfThoughtContext.Provider>
);
},
);
export type ChainOfThoughtHeaderProps = ComponentProps<
typeof CollapsibleTrigger
> & {
icon?: React.ReactElement;
};
export const ChainOfThoughtHeader = memo(
({ className, children, icon, ...props }: ChainOfThoughtHeaderProps) => {
const { isOpen, setIsOpen } = useChainOfThought();
return (
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
<CollapsibleTrigger
className={cn(
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
className,
)}
{...props}
>
{icon ?? <BrainIcon className="size-4" />}
<span className="flex-1 text-left">
{children ?? "Chain of Thought"}
</span>
<ChevronDownIcon
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0",
)}
/>
</CollapsibleTrigger>
</Collapsible>
);
},
);
export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
icon?: LucideIcon | React.ReactElement;
label: ReactNode;
description?: ReactNode;
status?: "complete" | "active" | "pending";
};
export const ChainOfThoughtStep = memo(
({
className,
icon: Icon = DotIcon,
label,
description,
status = "complete",
children,
...props
}: ChainOfThoughtStepProps) => {
const statusStyles = {
complete: "text-muted-foreground",
active: "text-foreground",
pending: "text-muted-foreground/50",
};
return (
<div
className={cn(
"flex gap-2 text-sm",
statusStyles[status],
"fade-in-0 slide-in-from-top-2 animate-in",
className,
)}
{...props}
>
<div className="relative mt-0.5">
{isValidElement(Icon) ? Icon : <Icon className="size-4" />}
<div className="bg-border absolute top-7 bottom-0 left-1/2 -mx-px w-px" />
</div>
<div className="flex-1 space-y-2 overflow-hidden">
<div>{label}</div>
{description && (
<div className="text-muted-foreground text-xs">{description}</div>
)}
{children}
</div>
</div>
);
},
);
export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">;
export const ChainOfThoughtSearchResults = memo(
({ className, ...props }: ChainOfThoughtSearchResultsProps) => (
<div
className={cn(
"flex flex-wrap items-center gap-2 overflow-x-hidden",
className,
)}
{...props}
/>
),
);
export type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>;
export const ChainOfThoughtSearchResult = memo(
({ className, children, ...props }: ChainOfThoughtSearchResultProps) => (
<Badge
className={cn("gap-1 px-2 py-0.5 text-xs font-normal", className)}
variant="secondary"
{...props}
>
{children}
</Badge>
),
);
export type ChainOfThoughtContentProps = ComponentProps<
typeof CollapsibleContent
>;
export const ChainOfThoughtContent = memo(
({ className, children, ...props }: ChainOfThoughtContentProps) => {
const { isOpen } = useChainOfThought();
return (
<Collapsible open={isOpen}>
<CollapsibleContent
className={cn(
"mt-2 space-y-3",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
className,
)}
{...props}
>
{children}
</CollapsibleContent>
</Collapsible>
);
},
);
export type ChainOfThoughtImageProps = ComponentProps<"div"> & {
caption?: string;
};
export const ChainOfThoughtImage = memo(
({ className, children, caption, ...props }: ChainOfThoughtImageProps) => (
<div className={cn("mt-2 space-y-2", className)} {...props}>
<div className="bg-muted relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg p-3">
{children}
</div>
{caption && <p className="text-muted-foreground text-xs">{caption}</p>}
</div>
),
);
ChainOfThought.displayName = "ChainOfThought";
ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader";
ChainOfThoughtStep.displayName = "ChainOfThoughtStep";
ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults";
ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult";
ChainOfThoughtContent.displayName = "ChainOfThoughtContent";
ChainOfThoughtImage.displayName = "ChainOfThoughtImage";
"use client";
import { useControllableState } from "@radix-ui/react-use-controllable-state";
import { Badge } from "@/components/ui/badge";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import {
BrainIcon,
ChevronDownIcon,
DotIcon,
type LucideIcon,
} from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import {
createContext,
isValidElement,
memo,
useContext,
useMemo,
} from "react";
type ChainOfThoughtContextValue = {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
};
const ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>(
null,
);
const useChainOfThought = () => {
const context = useContext(ChainOfThoughtContext);
if (!context) {
throw new Error(
"ChainOfThought components must be used within ChainOfThought",
);
}
return context;
};
export type ChainOfThoughtProps = ComponentProps<"div"> & {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
};
export const ChainOfThought = memo(
({
className,
open,
defaultOpen = false,
onOpenChange,
children,
...props
}: ChainOfThoughtProps) => {
const [isOpen, setIsOpen] = useControllableState({
prop: open,
defaultProp: defaultOpen,
onChange: onOpenChange,
});
const chainOfThoughtContext = useMemo(
() => ({ isOpen, setIsOpen }),
[isOpen, setIsOpen],
);
return (
<ChainOfThoughtContext.Provider value={chainOfThoughtContext}>
<div className={cn("not-prose", className)} {...props}>
{children}
</div>
</ChainOfThoughtContext.Provider>
);
},
);
export type ChainOfThoughtHeaderProps = ComponentProps<
typeof CollapsibleTrigger
> & {
icon?: React.ReactElement;
};
export const ChainOfThoughtHeader = memo(
({ className, children, icon, ...props }: ChainOfThoughtHeaderProps) => {
const { isOpen, setIsOpen } = useChainOfThought();
return (
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
<CollapsibleTrigger
className={cn(
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
className,
)}
{...props}
>
{icon ?? <BrainIcon className="size-4" />}
<span className="flex-1 text-left">
{children ?? "Chain of Thought"}
</span>
<ChevronDownIcon
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0",
)}
/>
</CollapsibleTrigger>
</Collapsible>
);
},
);
export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
icon?: LucideIcon | React.ReactElement;
label: ReactNode;
description?: ReactNode;
status?: "complete" | "active" | "pending";
};
export const ChainOfThoughtStep = memo(
({
className,
icon: Icon = DotIcon,
label,
description,
status = "complete",
children,
...props
}: ChainOfThoughtStepProps) => {
const statusStyles = {
complete: "text-muted-foreground",
active: "text-foreground",
pending: "text-muted-foreground/50",
};
return (
<div
className={cn(
"flex gap-2 text-sm",
statusStyles[status],
"fade-in-0 slide-in-from-top-2 animate-in",
className,
)}
{...props}
>
<div className="relative mt-0.5">
{isValidElement(Icon) ? Icon : <Icon className="size-4" />}
<div className="bg-border absolute top-7 bottom-0 left-1/2 -mx-px w-px" />
</div>
<div className="flex-1 space-y-2 overflow-hidden">
<div>{label}</div>
{description && (
<div className="text-muted-foreground text-xs">{description}</div>
)}
{children}
</div>
</div>
);
},
);
export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">;
export const ChainOfThoughtSearchResults = memo(
({ className, ...props }: ChainOfThoughtSearchResultsProps) => (
<div
className={cn(
"flex flex-wrap items-center gap-2 overflow-x-hidden",
className,
)}
{...props}
/>
),
);
export type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>;
export const ChainOfThoughtSearchResult = memo(
({ className, children, ...props }: ChainOfThoughtSearchResultProps) => (
<Badge
className={cn("gap-1 px-2 py-0.5 text-xs font-normal", className)}
variant="secondary"
{...props}
>
{children}
</Badge>
),
);
export type ChainOfThoughtContentProps = ComponentProps<
typeof CollapsibleContent
>;
export const ChainOfThoughtContent = memo(
({ className, children, ...props }: ChainOfThoughtContentProps) => {
const { isOpen } = useChainOfThought();
return (
<Collapsible open={isOpen}>
<CollapsibleContent
className={cn(
"mt-2 space-y-3",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
className,
)}
{...props}
>
{children}
</CollapsibleContent>
</Collapsible>
);
},
);
export type ChainOfThoughtImageProps = ComponentProps<"div"> & {
caption?: string;
};
export const ChainOfThoughtImage = memo(
({ className, children, caption, ...props }: ChainOfThoughtImageProps) => (
<div className={cn("mt-2 space-y-2", className)} {...props}>
<div className="bg-muted relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg p-3">
{children}
</div>
{caption && <p className="text-muted-foreground text-xs">{caption}</p>}
</div>
),
);
ChainOfThought.displayName = "ChainOfThought";
ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader";
ChainOfThoughtStep.displayName = "ChainOfThoughtStep";
ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults";
ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult";
ChainOfThoughtContent.displayName = "ChainOfThoughtContent";
ChainOfThoughtImage.displayName = "ChainOfThoughtImage";
@@ -1,71 +1,71 @@
"use client";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { BookmarkIcon, type LucideProps } from "lucide-react";
import type { ComponentProps, HTMLAttributes } from "react";
export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
export const Checkpoint = ({
className,
children,
...props
}: CheckpointProps) => (
<div
className={cn(
"text-muted-foreground flex items-center gap-0.5 overflow-hidden",
className,
)}
{...props}
>
{children}
<Separator />
</div>
);
export type CheckpointIconProps = LucideProps;
export const CheckpointIcon = ({
className,
children,
...props
}: CheckpointIconProps) =>
children ?? (
<BookmarkIcon className={cn("size-4 shrink-0", className)} {...props} />
);
export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
tooltip?: string;
};
export const CheckpointTrigger = ({
children,
className,
variant = "ghost",
size = "sm",
tooltip,
...props
}: CheckpointTriggerProps) =>
tooltip ? (
<Tooltip>
<TooltipTrigger asChild>
<Button size={size} type="button" variant={variant} {...props}>
{children}
</Button>
</TooltipTrigger>
<TooltipContent align="start" side="bottom">
{tooltip}
</TooltipContent>
</Tooltip>
) : (
<Button size={size} type="button" variant={variant} {...props}>
{children}
</Button>
);
"use client";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { BookmarkIcon, type LucideProps } from "lucide-react";
import type { ComponentProps, HTMLAttributes } from "react";
export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
export const Checkpoint = ({
className,
children,
...props
}: CheckpointProps) => (
<div
className={cn(
"text-muted-foreground flex items-center gap-0.5 overflow-hidden",
className,
)}
{...props}
>
{children}
<Separator />
</div>
);
export type CheckpointIconProps = LucideProps;
export const CheckpointIcon = ({
className,
children,
...props
}: CheckpointIconProps) =>
children ?? (
<BookmarkIcon className={cn("size-4 shrink-0", className)} {...props} />
);
export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
tooltip?: string;
};
export const CheckpointTrigger = ({
children,
className,
variant = "ghost",
size = "sm",
tooltip,
...props
}: CheckpointTriggerProps) =>
tooltip ? (
<Tooltip>
<TooltipTrigger asChild>
<Button size={size} type="button" variant={variant} {...props}>
{children}
</Button>
</TooltipTrigger>
<TooltipContent align="start" side="bottom">
{tooltip}
</TooltipContent>
</Tooltip>
) : (
<Button size={size} type="button" variant={variant} {...props}>
{children}
</Button>
);
@@ -1,178 +1,178 @@
"use client";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { CheckIcon, CopyIcon } from "lucide-react";
import {
type ComponentProps,
createContext,
type HTMLAttributes,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki";
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
code: string;
language: BundledLanguage;
showLineNumbers?: boolean;
};
type CodeBlockContextType = {
code: string;
};
const CodeBlockContext = createContext<CodeBlockContextType>({
code: "",
});
const lineNumberTransformer: ShikiTransformer = {
name: "line-numbers",
line(node, line) {
node.children.unshift({
type: "element",
tagName: "span",
properties: {
className: [
"inline-block",
"min-w-10",
"mr-4",
"text-right",
"select-none",
"text-muted-foreground",
],
},
children: [{ type: "text", value: String(line) }],
});
},
};
export async function highlightCode(
code: string,
language: BundledLanguage,
showLineNumbers = false,
) {
const transformers: ShikiTransformer[] = showLineNumbers
? [lineNumberTransformer]
: [];
return await Promise.all([
codeToHtml(code, {
lang: language,
theme: "one-light",
transformers,
}),
codeToHtml(code, {
lang: language,
theme: "one-dark-pro",
transformers,
}),
]);
}
export const CodeBlock = ({
code,
language,
showLineNumbers = false,
className,
children,
...props
}: CodeBlockProps) => {
const [html, setHtml] = useState<string>("");
const [darkHtml, setDarkHtml] = useState<string>("");
const mounted = useRef(false);
useEffect(() => {
highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
if (!mounted.current) {
setHtml(light);
setDarkHtml(dark);
mounted.current = true;
}
});
return () => {
mounted.current = false;
};
}, [code, language, showLineNumbers]);
return (
<CodeBlockContext.Provider value={{ code }}>
<div
className={cn(
"group bg-background text-foreground relative size-full overflow-hidden rounded-md border",
className,
)}
{...props}
>
<div className="relative size-full">
<div
className="[&>pre]:bg-background! [&>pre]:text-foreground! size-full overflow-auto dark:hidden [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
dangerouslySetInnerHTML={{ __html: html }}
/>
<div
className="[&>pre]:bg-background! [&>pre]:text-foreground! hidden size-full overflow-auto dark:block [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
dangerouslySetInnerHTML={{ __html: darkHtml }}
/>
{children && (
<div className="absolute top-2 right-2 flex items-center gap-2">
{children}
</div>
)}
</div>
</div>
</CodeBlockContext.Provider>
);
};
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
onCopy?: () => void;
onError?: (error: Error) => void;
timeout?: number;
};
export const CodeBlockCopyButton = ({
onCopy,
onError,
timeout = 2000,
children,
className,
...props
}: CodeBlockCopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false);
const { code } = useContext(CodeBlockContext);
const copyToClipboard = async () => {
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
onError?.(new Error("Clipboard API not available"));
return;
}
try {
await navigator.clipboard.writeText(code);
setIsCopied(true);
onCopy?.();
setTimeout(() => setIsCopied(false), timeout);
} catch (error) {
onError?.(error as Error);
}
};
const Icon = isCopied ? CheckIcon : CopyIcon;
return (
<Button
className={cn("shrink-0", className)}
onClick={copyToClipboard}
size="icon"
variant="ghost"
{...props}
>
{children ?? <Icon size={14} />}
</Button>
);
};
"use client";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { CheckIcon, CopyIcon } from "lucide-react";
import {
type ComponentProps,
createContext,
type HTMLAttributes,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki";
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
code: string;
language: BundledLanguage;
showLineNumbers?: boolean;
};
type CodeBlockContextType = {
code: string;
};
const CodeBlockContext = createContext<CodeBlockContextType>({
code: "",
});
const lineNumberTransformer: ShikiTransformer = {
name: "line-numbers",
line(node, line) {
node.children.unshift({
type: "element",
tagName: "span",
properties: {
className: [
"inline-block",
"min-w-10",
"mr-4",
"text-right",
"select-none",
"text-muted-foreground",
],
},
children: [{ type: "text", value: String(line) }],
});
},
};
export async function highlightCode(
code: string,
language: BundledLanguage,
showLineNumbers = false,
) {
const transformers: ShikiTransformer[] = showLineNumbers
? [lineNumberTransformer]
: [];
return await Promise.all([
codeToHtml(code, {
lang: language,
theme: "one-light",
transformers,
}),
codeToHtml(code, {
lang: language,
theme: "one-dark-pro",
transformers,
}),
]);
}
export const CodeBlock = ({
code,
language,
showLineNumbers = false,
className,
children,
...props
}: CodeBlockProps) => {
const [html, setHtml] = useState<string>("");
const [darkHtml, setDarkHtml] = useState<string>("");
const mounted = useRef(false);
useEffect(() => {
highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
if (!mounted.current) {
setHtml(light);
setDarkHtml(dark);
mounted.current = true;
}
});
return () => {
mounted.current = false;
};
}, [code, language, showLineNumbers]);
return (
<CodeBlockContext.Provider value={{ code }}>
<div
className={cn(
"group bg-background text-foreground relative size-full overflow-hidden rounded-md border",
className,
)}
{...props}
>
<div className="relative size-full">
<div
className="[&>pre]:bg-background! [&>pre]:text-foreground! size-full overflow-auto dark:hidden [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
dangerouslySetInnerHTML={{ __html: html }}
/>
<div
className="[&>pre]:bg-background! [&>pre]:text-foreground! hidden size-full overflow-auto dark:block [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
dangerouslySetInnerHTML={{ __html: darkHtml }}
/>
{children && (
<div className="absolute top-2 right-2 flex items-center gap-2">
{children}
</div>
)}
</div>
</div>
</CodeBlockContext.Provider>
);
};
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
onCopy?: () => void;
onError?: (error: Error) => void;
timeout?: number;
};
export const CodeBlockCopyButton = ({
onCopy,
onError,
timeout = 2000,
children,
className,
...props
}: CodeBlockCopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false);
const { code } = useContext(CodeBlockContext);
const copyToClipboard = async () => {
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
onError?.(new Error("Clipboard API not available"));
return;
}
try {
await navigator.clipboard.writeText(code);
setIsCopied(true);
onCopy?.();
setTimeout(() => setIsCopied(false), timeout);
} catch (error) {
onError?.(error as Error);
}
};
const Icon = isCopied ? CheckIcon : CopyIcon;
return (
<Button
className={cn("shrink-0", className)}
onClick={copyToClipboard}
size="icon"
variant="ghost"
{...props}
>
{children ?? <Icon size={14} />}
</Button>
);
};
@@ -1,28 +1,28 @@
import type { ConnectionLineComponent } from "@xyflow/react";
const HALF = 0.5;
export const Connection: ConnectionLineComponent = ({
fromX,
fromY,
toX,
toY,
}) => (
<g>
<path
className="animated"
d={`M${fromX},${fromY} C ${fromX + (toX - fromX) * HALF},${fromY} ${fromX + (toX - fromX) * HALF},${toY} ${toX},${toY}`}
fill="none"
stroke="var(--color-ring)"
strokeWidth={1}
/>
<circle
cx={toX}
cy={toY}
fill="#fff"
r={3}
stroke="var(--color-ring)"
strokeWidth={1}
/>
</g>
);
import type { ConnectionLineComponent } from "@xyflow/react";
const HALF = 0.5;
export const Connection: ConnectionLineComponent = ({
fromX,
fromY,
toX,
toY,
}) => (
<g>
<path
className="animated"
d={`M${fromX},${fromY} C ${fromX + (toX - fromX) * HALF},${fromY} ${fromX + (toX - fromX) * HALF},${toY} ${toX},${toY}`}
fill="none"
stroke="var(--color-ring)"
strokeWidth={1}
/>
<circle
cx={toX}
cy={toY}
fill="#fff"
r={3}
stroke="var(--color-ring)"
strokeWidth={1}
/>
</g>
);
@@ -1,408 +1,408 @@
"use client";
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
import type { LanguageModelUsage } from "ai";
import { type ComponentProps, createContext, useContext } from "react";
import { getUsage } from "tokenlens";
const PERCENT_MAX = 100;
const ICON_RADIUS = 10;
const ICON_VIEWBOX = 24;
const ICON_CENTER = 12;
const ICON_STROKE_WIDTH = 2;
type ModelId = string;
type ContextSchema = {
usedTokens: number;
maxTokens: number;
usage?: LanguageModelUsage;
modelId?: ModelId;
};
const ContextContext = createContext<ContextSchema | null>(null);
const useContextValue = () => {
const context = useContext(ContextContext);
if (!context) {
throw new Error("Context components must be used within Context");
}
return context;
};
export type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema;
export const Context = ({
usedTokens,
maxTokens,
usage,
modelId,
...props
}: ContextProps) => (
<ContextContext.Provider
value={{
usedTokens,
maxTokens,
usage,
modelId,
}}
>
<HoverCard closeDelay={0} openDelay={0} {...props} />
</ContextContext.Provider>
);
const ContextIcon = () => {
const { usedTokens, maxTokens } = useContextValue();
const circumference = 2 * Math.PI * ICON_RADIUS;
const usedPercent = usedTokens / maxTokens;
const dashOffset = circumference * (1 - usedPercent);
return (
<svg
aria-label="Model context usage"
height="20"
role="img"
style={{ color: "currentcolor" }}
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
width="20"
>
<circle
cx={ICON_CENTER}
cy={ICON_CENTER}
fill="none"
opacity="0.25"
r={ICON_RADIUS}
stroke="currentColor"
strokeWidth={ICON_STROKE_WIDTH}
/>
<circle
cx={ICON_CENTER}
cy={ICON_CENTER}
fill="none"
opacity="0.7"
r={ICON_RADIUS}
stroke="currentColor"
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={dashOffset}
strokeLinecap="round"
strokeWidth={ICON_STROKE_WIDTH}
style={{ transformOrigin: "center", transform: "rotate(-90deg)" }}
/>
</svg>
);
};
export type ContextTriggerProps = ComponentProps<typeof Button>;
export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
const { usedTokens, maxTokens } = useContextValue();
const usedPercent = usedTokens / maxTokens;
const renderedPercent = new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 1,
}).format(usedPercent);
return (
<HoverCardTrigger asChild>
{children ?? (
<Button type="button" variant="ghost" {...props}>
<span className="text-muted-foreground font-medium">
{renderedPercent}
</span>
<ContextIcon />
</Button>
)}
</HoverCardTrigger>
);
};
export type ContextContentProps = ComponentProps<typeof HoverCardContent>;
export const ContextContent = ({
className,
...props
}: ContextContentProps) => (
<HoverCardContent
className={cn("min-w-60 divide-y overflow-hidden p-0", className)}
{...props}
/>
);
export type ContextContentHeaderProps = ComponentProps<"div">;
export const ContextContentHeader = ({
children,
className,
...props
}: ContextContentHeaderProps) => {
const { usedTokens, maxTokens } = useContextValue();
const usedPercent = usedTokens / maxTokens;
const displayPct = new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 1,
}).format(usedPercent);
const used = new Intl.NumberFormat("en-US", {
notation: "compact",
}).format(usedTokens);
const total = new Intl.NumberFormat("en-US", {
notation: "compact",
}).format(maxTokens);
return (
<div className={cn("w-full space-y-2 p-3", className)} {...props}>
{children ?? (
<>
<div className="flex items-center justify-between gap-3 text-xs">
<p>{displayPct}</p>
<p className="text-muted-foreground font-mono">
{used} / {total}
</p>
</div>
<div className="space-y-2">
<Progress className="bg-muted" value={usedPercent * PERCENT_MAX} />
</div>
</>
)}
</div>
);
};
export type ContextContentBodyProps = ComponentProps<"div">;
export const ContextContentBody = ({
children,
className,
...props
}: ContextContentBodyProps) => (
<div className={cn("w-full p-3", className)} {...props}>
{children}
</div>
);
export type ContextContentFooterProps = ComponentProps<"div">;
export const ContextContentFooter = ({
children,
className,
...props
}: ContextContentFooterProps) => {
const { modelId, usage } = useContextValue();
const costUSD = modelId
? getUsage({
modelId,
usage: {
input: usage?.inputTokens ?? 0,
output: usage?.outputTokens ?? 0,
},
}).costUSD?.totalUSD
: undefined;
const totalCost = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(costUSD ?? 0);
return (
<div
className={cn(
"bg-secondary flex w-full items-center justify-between gap-3 p-3 text-xs",
className,
)}
{...props}
>
{children ?? (
<>
<span className="text-muted-foreground">Total cost</span>
<span>{totalCost}</span>
</>
)}
</div>
);
};
export type ContextInputUsageProps = ComponentProps<"div">;
export const ContextInputUsage = ({
className,
children,
...props
}: ContextInputUsageProps) => {
const { usage, modelId } = useContextValue();
const inputTokens = usage?.inputTokens ?? 0;
if (children) {
return children;
}
if (!inputTokens) {
return null;
}
const inputCost = modelId
? getUsage({
modelId,
usage: { input: inputTokens, output: 0 },
}).costUSD?.totalUSD
: undefined;
const inputCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(inputCost ?? 0);
return (
<div
className={cn("flex items-center justify-between text-xs", className)}
{...props}
>
<span className="text-muted-foreground">Input</span>
<TokensWithCost costText={inputCostText} tokens={inputTokens} />
</div>
);
};
export type ContextOutputUsageProps = ComponentProps<"div">;
export const ContextOutputUsage = ({
className,
children,
...props
}: ContextOutputUsageProps) => {
const { usage, modelId } = useContextValue();
const outputTokens = usage?.outputTokens ?? 0;
if (children) {
return children;
}
if (!outputTokens) {
return null;
}
const outputCost = modelId
? getUsage({
modelId,
usage: { input: 0, output: outputTokens },
}).costUSD?.totalUSD
: undefined;
const outputCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(outputCost ?? 0);
return (
<div
className={cn("flex items-center justify-between text-xs", className)}
{...props}
>
<span className="text-muted-foreground">Output</span>
<TokensWithCost costText={outputCostText} tokens={outputTokens} />
</div>
);
};
export type ContextReasoningUsageProps = ComponentProps<"div">;
export const ContextReasoningUsage = ({
className,
children,
...props
}: ContextReasoningUsageProps) => {
const { usage, modelId } = useContextValue();
const reasoningTokens = usage?.reasoningTokens ?? 0;
if (children) {
return children;
}
if (!reasoningTokens) {
return null;
}
const reasoningCost = modelId
? getUsage({
modelId,
usage: { reasoningTokens },
}).costUSD?.totalUSD
: undefined;
const reasoningCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(reasoningCost ?? 0);
return (
<div
className={cn("flex items-center justify-between text-xs", className)}
{...props}
>
<span className="text-muted-foreground">Reasoning</span>
<TokensWithCost costText={reasoningCostText} tokens={reasoningTokens} />
</div>
);
};
export type ContextCacheUsageProps = ComponentProps<"div">;
export const ContextCacheUsage = ({
className,
children,
...props
}: ContextCacheUsageProps) => {
const { usage, modelId } = useContextValue();
const cacheTokens = usage?.cachedInputTokens ?? 0;
if (children) {
return children;
}
if (!cacheTokens) {
return null;
}
const cacheCost = modelId
? getUsage({
modelId,
usage: { cacheReads: cacheTokens, input: 0, output: 0 },
}).costUSD?.totalUSD
: undefined;
const cacheCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(cacheCost ?? 0);
return (
<div
className={cn("flex items-center justify-between text-xs", className)}
{...props}
>
<span className="text-muted-foreground">Cache</span>
<TokensWithCost costText={cacheCostText} tokens={cacheTokens} />
</div>
);
};
const TokensWithCost = ({
tokens,
costText,
}: {
tokens?: number;
costText?: string;
}) => (
<span>
{tokens === undefined
? "—"
: new Intl.NumberFormat("en-US", {
notation: "compact",
}).format(tokens)}
{costText ? (
<span className="text-muted-foreground ml-2">• {costText}</span>
) : null}
</span>
);
"use client";
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
import type { LanguageModelUsage } from "ai";
import { type ComponentProps, createContext, useContext } from "react";
import { getUsage } from "tokenlens";
const PERCENT_MAX = 100;
const ICON_RADIUS = 10;
const ICON_VIEWBOX = 24;
const ICON_CENTER = 12;
const ICON_STROKE_WIDTH = 2;
type ModelId = string;
type ContextSchema = {
usedTokens: number;
maxTokens: number;
usage?: LanguageModelUsage;
modelId?: ModelId;
};
const ContextContext = createContext<ContextSchema | null>(null);
const useContextValue = () => {
const context = useContext(ContextContext);
if (!context) {
throw new Error("Context components must be used within Context");
}
return context;
};
export type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema;
export const Context = ({
usedTokens,
maxTokens,
usage,
modelId,
...props
}: ContextProps) => (
<ContextContext.Provider
value={{
usedTokens,
maxTokens,
usage,
modelId,
}}
>
<HoverCard closeDelay={0} openDelay={0} {...props} />
</ContextContext.Provider>
);
const ContextIcon = () => {
const { usedTokens, maxTokens } = useContextValue();
const circumference = 2 * Math.PI * ICON_RADIUS;
const usedPercent = usedTokens / maxTokens;
const dashOffset = circumference * (1 - usedPercent);
return (
<svg
aria-label="Model context usage"
height="20"
role="img"
style={{ color: "currentcolor" }}
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
width="20"
>
<circle
cx={ICON_CENTER}
cy={ICON_CENTER}
fill="none"
opacity="0.25"
r={ICON_RADIUS}
stroke="currentColor"
strokeWidth={ICON_STROKE_WIDTH}
/>
<circle
cx={ICON_CENTER}
cy={ICON_CENTER}
fill="none"
opacity="0.7"
r={ICON_RADIUS}
stroke="currentColor"
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={dashOffset}
strokeLinecap="round"
strokeWidth={ICON_STROKE_WIDTH}
style={{ transformOrigin: "center", transform: "rotate(-90deg)" }}
/>
</svg>
);
};
export type ContextTriggerProps = ComponentProps<typeof Button>;
export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
const { usedTokens, maxTokens } = useContextValue();
const usedPercent = usedTokens / maxTokens;
const renderedPercent = new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 1,
}).format(usedPercent);
return (
<HoverCardTrigger asChild>
{children ?? (
<Button type="button" variant="ghost" {...props}>
<span className="text-muted-foreground font-medium">
{renderedPercent}
</span>
<ContextIcon />
</Button>
)}
</HoverCardTrigger>
);
};
export type ContextContentProps = ComponentProps<typeof HoverCardContent>;
export const ContextContent = ({
className,
...props
}: ContextContentProps) => (
<HoverCardContent
className={cn("min-w-60 divide-y overflow-hidden p-0", className)}
{...props}
/>
);
export type ContextContentHeaderProps = ComponentProps<"div">;
export const ContextContentHeader = ({
children,
className,
...props
}: ContextContentHeaderProps) => {
const { usedTokens, maxTokens } = useContextValue();
const usedPercent = usedTokens / maxTokens;
const displayPct = new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 1,
}).format(usedPercent);
const used = new Intl.NumberFormat("en-US", {
notation: "compact",
}).format(usedTokens);
const total = new Intl.NumberFormat("en-US", {
notation: "compact",
}).format(maxTokens);
return (
<div className={cn("w-full space-y-2 p-3", className)} {...props}>
{children ?? (
<>
<div className="flex items-center justify-between gap-3 text-xs">
<p>{displayPct}</p>
<p className="text-muted-foreground font-mono">
{used} / {total}
</p>
</div>
<div className="space-y-2">
<Progress className="bg-muted" value={usedPercent * PERCENT_MAX} />
</div>
</>
)}
</div>
);
};
export type ContextContentBodyProps = ComponentProps<"div">;
export const ContextContentBody = ({
children,
className,
...props
}: ContextContentBodyProps) => (
<div className={cn("w-full p-3", className)} {...props}>
{children}
</div>
);
export type ContextContentFooterProps = ComponentProps<"div">;
export const ContextContentFooter = ({
children,
className,
...props
}: ContextContentFooterProps) => {
const { modelId, usage } = useContextValue();
const costUSD = modelId
? getUsage({
modelId,
usage: {
input: usage?.inputTokens ?? 0,
output: usage?.outputTokens ?? 0,
},
}).costUSD?.totalUSD
: undefined;
const totalCost = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(costUSD ?? 0);
return (
<div
className={cn(
"bg-secondary flex w-full items-center justify-between gap-3 p-3 text-xs",
className,
)}
{...props}
>
{children ?? (
<>
<span className="text-muted-foreground">Total cost</span>
<span>{totalCost}</span>
</>
)}
</div>
);
};
export type ContextInputUsageProps = ComponentProps<"div">;
export const ContextInputUsage = ({
className,
children,
...props
}: ContextInputUsageProps) => {
const { usage, modelId } = useContextValue();
const inputTokens = usage?.inputTokens ?? 0;
if (children) {
return children;
}
if (!inputTokens) {
return null;
}
const inputCost = modelId
? getUsage({
modelId,
usage: { input: inputTokens, output: 0 },
}).costUSD?.totalUSD
: undefined;
const inputCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(inputCost ?? 0);
return (
<div
className={cn("flex items-center justify-between text-xs", className)}
{...props}
>
<span className="text-muted-foreground">Input</span>
<TokensWithCost costText={inputCostText} tokens={inputTokens} />
</div>
);
};
export type ContextOutputUsageProps = ComponentProps<"div">;
export const ContextOutputUsage = ({
className,
children,
...props
}: ContextOutputUsageProps) => {
const { usage, modelId } = useContextValue();
const outputTokens = usage?.outputTokens ?? 0;
if (children) {
return children;
}
if (!outputTokens) {
return null;
}
const outputCost = modelId
? getUsage({
modelId,
usage: { input: 0, output: outputTokens },
}).costUSD?.totalUSD
: undefined;
const outputCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(outputCost ?? 0);
return (
<div
className={cn("flex items-center justify-between text-xs", className)}
{...props}
>
<span className="text-muted-foreground">Output</span>
<TokensWithCost costText={outputCostText} tokens={outputTokens} />
</div>
);
};
export type ContextReasoningUsageProps = ComponentProps<"div">;
export const ContextReasoningUsage = ({
className,
children,
...props
}: ContextReasoningUsageProps) => {
const { usage, modelId } = useContextValue();
const reasoningTokens = usage?.reasoningTokens ?? 0;
if (children) {
return children;
}
if (!reasoningTokens) {
return null;
}
const reasoningCost = modelId
? getUsage({
modelId,
usage: { reasoningTokens },
}).costUSD?.totalUSD
: undefined;
const reasoningCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(reasoningCost ?? 0);
return (
<div
className={cn("flex items-center justify-between text-xs", className)}
{...props}
>
<span className="text-muted-foreground">Reasoning</span>
<TokensWithCost costText={reasoningCostText} tokens={reasoningTokens} />
</div>
);
};
export type ContextCacheUsageProps = ComponentProps<"div">;
export const ContextCacheUsage = ({
className,
children,
...props
}: ContextCacheUsageProps) => {
const { usage, modelId } = useContextValue();
const cacheTokens = usage?.cachedInputTokens ?? 0;
if (children) {
return children;
}
if (!cacheTokens) {
return null;
}
const cacheCost = modelId
? getUsage({
modelId,
usage: { cacheReads: cacheTokens, input: 0, output: 0 },
}).costUSD?.totalUSD
: undefined;
const cacheCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(cacheCost ?? 0);
return (
<div
className={cn("flex items-center justify-between text-xs", className)}
{...props}
>
<span className="text-muted-foreground">Cache</span>
<TokensWithCost costText={cacheCostText} tokens={cacheTokens} />
</div>
);
};
const TokensWithCost = ({
tokens,
costText,
}: {
tokens?: number;
costText?: string;
}) => (
<span>
{tokens === undefined
? "—"
: new Intl.NumberFormat("en-US", {
notation: "compact",
}).format(tokens)}
{costText ? (
<span className="text-muted-foreground ml-2">• {costText}</span>
) : null}
</span>
);
@@ -1,18 +1,18 @@
"use client";
import { cn } from "@/lib/utils";
import { Controls as ControlsPrimitive } from "@xyflow/react";
import type { ComponentProps } from "react";
export type ControlsProps = ComponentProps<typeof ControlsPrimitive>;
export const Controls = ({ className, ...props }: ControlsProps) => (
<ControlsPrimitive
className={cn(
"bg-card gap-px overflow-hidden rounded-md border p-1 shadow-none!",
"[&>button]:hover:bg-secondary! [&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent!",
className,
)}
{...props}
/>
);
"use client";
import { cn } from "@/lib/utils";
import { Controls as ControlsPrimitive } from "@xyflow/react";
import type { ComponentProps } from "react";
export type ControlsProps = ComponentProps<typeof ControlsPrimitive>;
export const Controls = ({ className, ...props }: ControlsProps) => (
<ControlsPrimitive
className={cn(
"bg-card gap-px overflow-hidden rounded-md border p-1 shadow-none!",
"[&>button]:hover:bg-secondary! [&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent!",
className,
)}
{...props}
/>
);
@@ -1,100 +1,100 @@
"use client";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ArrowDownIcon } from "lucide-react";
import type { ComponentProps } from "react";
import { useCallback } from "react";
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
export type ConversationProps = ComponentProps<typeof StickToBottom>;
export const Conversation = ({ className, ...props }: ConversationProps) => (
<StickToBottom
className={cn("relative flex-1 overflow-y-hidden", className)}
initial="smooth"
resize="smooth"
role="log"
{...props}
/>
);
export type ConversationContentProps = ComponentProps<
typeof StickToBottom.Content
>;
export const ConversationContent = ({
className,
...props
}: ConversationContentProps) => (
<StickToBottom.Content
className={cn("flex flex-col gap-8 p-4", className)}
{...props}
/>
);
export type ConversationEmptyStateProps = ComponentProps<"div"> & {
title?: string;
description?: string;
icon?: React.ReactNode;
};
export const ConversationEmptyState = ({
className,
title = "No messages yet",
description = "Start a conversation to see messages here",
icon,
children,
...props
}: ConversationEmptyStateProps) => (
<div
className={cn(
"flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
className,
)}
{...props}
>
{children ?? (
<>
{icon && <div className="text-muted-foreground">{icon}</div>}
<div className="space-y-1">
<h3 className="text-sm font-medium">{title}</h3>
{description && (
<p className="text-muted-foreground text-sm">{description}</p>
)}
</div>
</>
)}
</div>
);
export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
export const ConversationScrollButton = ({
className,
...props
}: ConversationScrollButtonProps) => {
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
const handleScrollToBottom = useCallback(() => {
scrollToBottom();
}, [scrollToBottom]);
return (
!isAtBottom && (
<Button
className={cn(
"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full",
className,
)}
onClick={handleScrollToBottom}
size="icon"
type="button"
variant="outline"
{...props}
>
<ArrowDownIcon className="size-4" />
</Button>
)
);
};
"use client";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ArrowDownIcon } from "lucide-react";
import type { ComponentProps } from "react";
import { useCallback } from "react";
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
export type ConversationProps = ComponentProps<typeof StickToBottom>;
export const Conversation = ({ className, ...props }: ConversationProps) => (
<StickToBottom
className={cn("relative flex-1 overflow-y-hidden", className)}
initial="smooth"
resize="smooth"
role="log"
{...props}
/>
);
export type ConversationContentProps = ComponentProps<
typeof StickToBottom.Content
>;
export const ConversationContent = ({
className,
...props
}: ConversationContentProps) => (
<StickToBottom.Content
className={cn("flex flex-col gap-8 p-4", className)}
{...props}
/>
);
export type ConversationEmptyStateProps = ComponentProps<"div"> & {
title?: string;
description?: string;
icon?: React.ReactNode;
};
export const ConversationEmptyState = ({
className,
title = "No messages yet",
description = "Start a conversation to see messages here",
icon,
children,
...props
}: ConversationEmptyStateProps) => (
<div
className={cn(
"flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
className,
)}
{...props}
>
{children ?? (
<>
{icon && <div className="text-muted-foreground">{icon}</div>}
<div className="space-y-1">
<h3 className="text-sm font-medium">{title}</h3>
{description && (
<p className="text-muted-foreground text-sm">{description}</p>
)}
</div>
</>
)}
</div>
);
export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
export const ConversationScrollButton = ({
className,
...props
}: ConversationScrollButtonProps) => {
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
const handleScrollToBottom = useCallback(() => {
scrollToBottom();
}, [scrollToBottom]);
return (
!isAtBottom && (
<Button
className={cn(
"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full",
className,
)}
onClick={handleScrollToBottom}
size="icon"
type="button"
variant="outline"
{...props}
>
<ArrowDownIcon className="size-4" />
</Button>
)
);
};
+140 -140
View File
@@ -1,140 +1,140 @@
import {
BaseEdge,
type EdgeProps,
getBezierPath,
getSimpleBezierPath,
type InternalNode,
type Node,
Position,
useInternalNode,
} from "@xyflow/react";
const Temporary = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
}: EdgeProps) => {
const [edgePath] = getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<BaseEdge
className="stroke-ring stroke-1"
id={id}
path={edgePath}
style={{
strokeDasharray: "5, 5",
}}
/>
);
};
const getHandleCoordsByPosition = (
node: InternalNode<Node>,
handlePosition: Position,
) => {
// Choose the handle type based on position - Left is for target, Right is for source
const handleType = handlePosition === Position.Left ? "target" : "source";
const handle = node.internals.handleBounds?.[handleType]?.find(
(h) => h.position === handlePosition,
);
if (!handle) {
return [0, 0] as const;
}
let offsetX = handle.width / 2;
let offsetY = handle.height / 2;
// this is a tiny detail to make the markerEnd of an edge visible.
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
switch (handlePosition) {
case Position.Left:
offsetX = 0;
break;
case Position.Right:
offsetX = handle.width;
break;
case Position.Top:
offsetY = 0;
break;
case Position.Bottom:
offsetY = handle.height;
break;
default:
throw new Error(`Invalid handle position: ${handlePosition}`);
}
const x = node.internals.positionAbsolute.x + handle.x + offsetX;
const y = node.internals.positionAbsolute.y + handle.y + offsetY;
return [x, y] as const;
};
const getEdgeParams = (
source: InternalNode<Node>,
target: InternalNode<Node>,
) => {
const sourcePos = Position.Right;
const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
const targetPos = Position.Left;
const [tx, ty] = getHandleCoordsByPosition(target, targetPos);
return {
sx,
sy,
tx,
ty,
sourcePos,
targetPos,
};
};
const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
const sourceNode = useInternalNode(source);
const targetNode = useInternalNode(target);
if (!(sourceNode && targetNode)) {
return null;
}
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
sourceNode,
targetNode,
);
const [edgePath] = getBezierPath({
sourceX: sx,
sourceY: sy,
sourcePosition: sourcePos,
targetX: tx,
targetY: ty,
targetPosition: targetPos,
});
return (
<>
<BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} />
<circle fill="var(--primary)" r="4">
<animateMotion dur="2s" path={edgePath} repeatCount="indefinite" />
</circle>
</>
);
};
export const Edge = {
Temporary,
Animated,
};
import {
BaseEdge,
type EdgeProps,
getBezierPath,
getSimpleBezierPath,
type InternalNode,
type Node,
Position,
useInternalNode,
} from "@xyflow/react";
const Temporary = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
}: EdgeProps) => {
const [edgePath] = getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<BaseEdge
className="stroke-ring stroke-1"
id={id}
path={edgePath}
style={{
strokeDasharray: "5, 5",
}}
/>
);
};
const getHandleCoordsByPosition = (
node: InternalNode<Node>,
handlePosition: Position,
) => {
// Choose the handle type based on position - Left is for target, Right is for source
const handleType = handlePosition === Position.Left ? "target" : "source";
const handle = node.internals.handleBounds?.[handleType]?.find(
(h) => h.position === handlePosition,
);
if (!handle) {
return [0, 0] as const;
}
let offsetX = handle.width / 2;
let offsetY = handle.height / 2;
// this is a tiny detail to make the markerEnd of an edge visible.
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
switch (handlePosition) {
case Position.Left:
offsetX = 0;
break;
case Position.Right:
offsetX = handle.width;
break;
case Position.Top:
offsetY = 0;
break;
case Position.Bottom:
offsetY = handle.height;
break;
default:
throw new Error(`Invalid handle position: ${handlePosition}`);
}
const x = node.internals.positionAbsolute.x + handle.x + offsetX;
const y = node.internals.positionAbsolute.y + handle.y + offsetY;
return [x, y] as const;
};
const getEdgeParams = (
source: InternalNode<Node>,
target: InternalNode<Node>,
) => {
const sourcePos = Position.Right;
const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
const targetPos = Position.Left;
const [tx, ty] = getHandleCoordsByPosition(target, targetPos);
return {
sx,
sy,
tx,
ty,
sourcePos,
targetPos,
};
};
const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
const sourceNode = useInternalNode(source);
const targetNode = useInternalNode(target);
if (!(sourceNode && targetNode)) {
return null;
}
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
sourceNode,
targetNode,
);
const [edgePath] = getBezierPath({
sourceX: sx,
sourceY: sy,
sourcePosition: sourcePos,
targetX: tx,
targetY: ty,
targetPosition: targetPos,
});
return (
<>
<BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} />
<circle fill="var(--primary)" r="4">
<animateMotion dur="2s" path={edgePath} repeatCount="indefinite" />
</circle>
</>
);
};
export const Edge = {
Temporary,
Animated,
};
@@ -1,24 +1,24 @@
import { cn } from "@/lib/utils";
import type { Experimental_GeneratedImage } from "ai";
export type ImageProps = Experimental_GeneratedImage & {
className?: string;
alt?: string;
};
export const Image = ({
base64,
uint8Array,
mediaType,
...props
}: ImageProps) => (
<img
{...props}
alt={props.alt}
className={cn(
"h-auto max-w-full overflow-hidden rounded-md",
props.className,
)}
src={`data:${mediaType};base64,${base64}`}
/>
);
import { cn } from "@/lib/utils";
import type { Experimental_GeneratedImage } from "ai";
export type ImageProps = Experimental_GeneratedImage & {
className?: string;
alt?: string;
};
export const Image = ({
base64,
uint8Array,
mediaType,
...props
}: ImageProps) => (
<img
{...props}
alt={props.alt}
className={cn(
"h-auto max-w-full overflow-hidden rounded-md",
props.className,
)}
src={`data:${mediaType};base64,${base64}`}
/>
);
@@ -1,96 +1,96 @@
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "react";
type LoaderIconProps = {
size?: number;
};
const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
<svg
height={size}
strokeLinejoin="round"
style={{ color: "currentcolor" }}
viewBox="0 0 16 16"
width={size}
>
<title>Loader</title>
<g clipPath="url(#clip0_2393_1490)">
<path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" />
<path
d="M8 16V12"
opacity="0.5"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M3.29773 1.52783L5.64887 4.7639"
opacity="0.9"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M12.7023 1.52783L10.3511 4.7639"
opacity="0.1"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M12.7023 14.472L10.3511 11.236"
opacity="0.4"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M3.29773 14.472L5.64887 11.236"
opacity="0.6"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M15.6085 5.52783L11.8043 6.7639"
opacity="0.2"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M0.391602 10.472L4.19583 9.23598"
opacity="0.7"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M15.6085 10.4722L11.8043 9.2361"
opacity="0.3"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M0.391602 5.52783L4.19583 6.7639"
opacity="0.8"
stroke="currentColor"
strokeWidth="1.5"
/>
</g>
<defs>
<clipPath id="clip0_2393_1490">
<rect fill="white" height="16" width="16" />
</clipPath>
</defs>
</svg>
);
export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
size?: number;
};
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
<div
className={cn(
"inline-flex animate-spin items-center justify-center",
className,
)}
{...props}
>
<LoaderIcon size={size} />
</div>
);
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "react";
type LoaderIconProps = {
size?: number;
};
const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
<svg
height={size}
strokeLinejoin="round"
style={{ color: "currentcolor" }}
viewBox="0 0 16 16"
width={size}
>
<title>Loader</title>
<g clipPath="url(#clip0_2393_1490)">
<path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" />
<path
d="M8 16V12"
opacity="0.5"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M3.29773 1.52783L5.64887 4.7639"
opacity="0.9"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M12.7023 1.52783L10.3511 4.7639"
opacity="0.1"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M12.7023 14.472L10.3511 11.236"
opacity="0.4"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M3.29773 14.472L5.64887 11.236"
opacity="0.6"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M15.6085 5.52783L11.8043 6.7639"
opacity="0.2"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M0.391602 10.472L4.19583 9.23598"
opacity="0.7"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M15.6085 10.4722L11.8043 9.2361"
opacity="0.3"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M0.391602 5.52783L4.19583 6.7639"
opacity="0.8"
stroke="currentColor"
strokeWidth="1.5"
/>
</g>
<defs>
<clipPath id="clip0_2393_1490">
<rect fill="white" height="16" width="16" />
</clipPath>
</defs>
</svg>
);
export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
size?: number;
};
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
<div
className={cn(
"inline-flex animate-spin items-center justify-center",
className,
)}
{...props}
>
<LoaderIcon size={size} />
</div>
);
@@ -1,446 +1,446 @@
"use client";
import { Button } from "@/components/ui/button";
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import type { FileUIPart, UIMessage } from "ai";
import {
ChevronLeftIcon,
ChevronRightIcon,
PaperclipIcon,
XIcon,
} from "lucide-react";
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
import { createContext, memo, useContext, useEffect, useState } from "react";
import { Streamdown } from "streamdown";
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"];
};
export const Message = ({ className, from, ...props }: MessageProps) => (
<div
className={cn(
"group flex w-full flex-col gap-2",
from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
className,
)}
{...props}
/>
);
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
export const MessageContent = ({
children,
className,
...props
}: MessageContentProps) => (
<div
className={cn(
"is-user:dark flex w-fit max-w-full min-w-0 flex-col gap-2 overflow-visible",
"group-[.is-user]:overflow-hidden",
"group-[.is-user]:bg-secondary group-[.is-user]:text-foreground group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:px-4 group-[.is-user]:py-3",
"group-[.is-assistant]:text-foreground",
className,
)}
{...props}
>
{children}
</div>
);
export type MessageActionsProps = ComponentProps<"div">;
export const MessageActions = ({
className,
children,
...props
}: MessageActionsProps) => (
<div className={cn("flex items-center gap-1", className)} {...props}>
{children}
</div>
);
export type MessageActionProps = ComponentProps<typeof Button> & {
tooltip?: string;
label?: string;
};
export const MessageAction = ({
tooltip,
children,
label,
variant = "ghost",
size = "icon-sm",
...props
}: MessageActionProps) => {
const button = (
<Button size={size} type="button" variant={variant} {...props}>
{children}
<span className="sr-only">{label || tooltip}</span>
</Button>
);
if (tooltip) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return button;
};
type MessageBranchContextType = {
currentBranch: number;
totalBranches: number;
goToPrevious: () => void;
goToNext: () => void;
branches: ReactElement[];
setBranches: (branches: ReactElement[]) => void;
};
const MessageBranchContext = createContext<MessageBranchContextType | null>(
null,
);
const useMessageBranch = () => {
const context = useContext(MessageBranchContext);
if (!context) {
throw new Error(
"MessageBranch components must be used within MessageBranch",
);
}
return context;
};
export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
defaultBranch?: number;
onBranchChange?: (branchIndex: number) => void;
};
export const MessageBranch = ({
defaultBranch = 0,
onBranchChange,
className,
...props
}: MessageBranchProps) => {
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
const [branches, setBranches] = useState<ReactElement[]>([]);
const handleBranchChange = (newBranch: number) => {
setCurrentBranch(newBranch);
onBranchChange?.(newBranch);
};
const goToPrevious = () => {
const newBranch =
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
handleBranchChange(newBranch);
};
const goToNext = () => {
const newBranch =
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
handleBranchChange(newBranch);
};
const contextValue: MessageBranchContextType = {
currentBranch,
totalBranches: branches.length,
goToPrevious,
goToNext,
branches,
setBranches,
};
return (
<MessageBranchContext.Provider value={contextValue}>
<div
className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
{...props}
/>
</MessageBranchContext.Provider>
);
};
export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
export const MessageBranchContent = ({
children,
...props
}: MessageBranchContentProps) => {
const { currentBranch, setBranches, branches } = useMessageBranch();
const childrenArray = Array.isArray(children) ? children : [children];
// Use useEffect to update branches when they change
useEffect(() => {
if (branches.length !== childrenArray.length) {
setBranches(childrenArray);
}
}, [childrenArray, branches, setBranches]);
return childrenArray.map((branch, index) => (
<div
className={cn(
"grid gap-2 overflow-hidden [&>div]:pb-0",
index === currentBranch ? "block" : "hidden",
)}
key={branch.key}
{...props}
>
{branch}
</div>
));
};
export type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"];
};
export const MessageBranchSelector = ({
className,
from,
...props
}: MessageBranchSelectorProps) => {
const { totalBranches } = useMessageBranch();
// Don't render if there's only one branch
if (totalBranches <= 1) {
return null;
}
return (
<ButtonGroup
className="[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md"
orientation="horizontal"
{...props}
/>
);
};
export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
export const MessageBranchPrevious = ({
children,
...props
}: MessageBranchPreviousProps) => {
const { goToPrevious, totalBranches } = useMessageBranch();
return (
<Button
aria-label="Previous branch"
disabled={totalBranches <= 1}
onClick={goToPrevious}
size="icon-sm"
type="button"
variant="ghost"
{...props}
>
{children ?? <ChevronLeftIcon size={14} />}
</Button>
);
};
export type MessageBranchNextProps = ComponentProps<typeof Button>;
export const MessageBranchNext = ({
children,
className,
...props
}: MessageBranchNextProps) => {
const { goToNext, totalBranches } = useMessageBranch();
return (
<Button
aria-label="Next branch"
disabled={totalBranches <= 1}
onClick={goToNext}
size="icon-sm"
type="button"
variant="ghost"
{...props}
>
{children ?? <ChevronRightIcon size={14} />}
</Button>
);
};
export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
export const MessageBranchPage = ({
className,
...props
}: MessageBranchPageProps) => {
const { currentBranch, totalBranches } = useMessageBranch();
return (
<ButtonGroupText
className={cn(
"text-muted-foreground border-none bg-transparent shadow-none",
className,
)}
{...props}
>
{currentBranch + 1} of {totalBranches}
</ButtonGroupText>
);
};
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
export const MessageResponse = memo(
({ className, ...props }: MessageResponseProps) => (
<Streamdown
className={cn(
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
className,
)}
{...props}
/>
),
(prevProps, nextProps) => prevProps.children === nextProps.children,
);
MessageResponse.displayName = "MessageResponse";
export type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
data: FileUIPart;
className?: string;
onRemove?: () => void;
};
export function MessageAttachment({
data,
className,
onRemove,
...props
}: MessageAttachmentProps) {
const filename = data.filename || "";
const mediaType =
data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
const isImage = mediaType === "image";
const attachmentLabel = filename || (isImage ? "Image" : "Attachment");
return (
<div
className={cn(
"group relative size-24 overflow-hidden rounded-lg",
className,
)}
{...props}
>
{isImage ? (
<>
<img
alt={filename || "attachment"}
className="size-full object-cover"
height={100}
src={data.url}
width={100}
/>
{onRemove && (
<Button
aria-label="Remove attachment"
className="bg-background/80 hover:bg-background absolute top-2 right-2 size-6 rounded-full p-0 opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100 [&>svg]:size-3"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
type="button"
variant="ghost"
>
<XIcon />
<span className="sr-only">Remove</span>
</Button>
)}
</>
) : (
<>
<Tooltip>
<TooltipTrigger asChild>
<div className="bg-muted text-muted-foreground flex size-full shrink-0 items-center justify-center rounded-lg">
<PaperclipIcon className="size-4" />
</div>
</TooltipTrigger>
<TooltipContent>
<p>{attachmentLabel}</p>
</TooltipContent>
</Tooltip>
{onRemove && (
<Button
aria-label="Remove attachment"
className="hover:bg-accent size-6 shrink-0 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100 [&>svg]:size-3"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
type="button"
variant="ghost"
>
<XIcon />
<span className="sr-only">Remove</span>
</Button>
)}
</>
)}
</div>
);
}
export type MessageAttachmentsProps = ComponentProps<"div">;
export function MessageAttachments({
children,
className,
...props
}: MessageAttachmentsProps) {
if (!children) {
return null;
}
return (
<div
className={cn(
"ml-auto flex w-fit flex-wrap items-start gap-2",
className,
)}
{...props}
>
{children}
</div>
);
}
export type MessageToolbarProps = ComponentProps<"div">;
export const MessageToolbar = ({
className,
children,
...props
}: MessageToolbarProps) => (
<div
className={cn(
"mt-4 flex w-full items-center justify-between gap-4",
className,
)}
{...props}
>
{children}
</div>
);
"use client";
import { Button } from "@/components/ui/button";
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import type { FileUIPart, UIMessage } from "ai";
import {
ChevronLeftIcon,
ChevronRightIcon,
PaperclipIcon,
XIcon,
} from "lucide-react";
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
import { createContext, memo, useContext, useEffect, useState } from "react";
import { Streamdown } from "streamdown";
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"];
};
export const Message = ({ className, from, ...props }: MessageProps) => (
<div
className={cn(
"group flex w-full flex-col gap-2",
from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
className,
)}
{...props}
/>
);
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
export const MessageContent = ({
children,
className,
...props
}: MessageContentProps) => (
<div
className={cn(
"is-user:dark flex w-fit max-w-full min-w-0 flex-col gap-2 overflow-visible",
"group-[.is-user]:overflow-hidden",
"group-[.is-user]:bg-secondary group-[.is-user]:text-foreground group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:px-4 group-[.is-user]:py-3",
"group-[.is-assistant]:text-foreground",
className,
)}
{...props}
>
{children}
</div>
);
export type MessageActionsProps = ComponentProps<"div">;
export const MessageActions = ({
className,
children,
...props
}: MessageActionsProps) => (
<div className={cn("flex items-center gap-1", className)} {...props}>
{children}
</div>
);
export type MessageActionProps = ComponentProps<typeof Button> & {
tooltip?: string;
label?: string;
};
export const MessageAction = ({
tooltip,
children,
label,
variant = "ghost",
size = "icon-sm",
...props
}: MessageActionProps) => {
const button = (
<Button size={size} type="button" variant={variant} {...props}>
{children}
<span className="sr-only">{label || tooltip}</span>
</Button>
);
if (tooltip) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return button;
};
type MessageBranchContextType = {
currentBranch: number;
totalBranches: number;
goToPrevious: () => void;
goToNext: () => void;
branches: ReactElement[];
setBranches: (branches: ReactElement[]) => void;
};
const MessageBranchContext = createContext<MessageBranchContextType | null>(
null,
);
const useMessageBranch = () => {
const context = useContext(MessageBranchContext);
if (!context) {
throw new Error(
"MessageBranch components must be used within MessageBranch",
);
}
return context;
};
export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
defaultBranch?: number;
onBranchChange?: (branchIndex: number) => void;
};
export const MessageBranch = ({
defaultBranch = 0,
onBranchChange,
className,
...props
}: MessageBranchProps) => {
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
const [branches, setBranches] = useState<ReactElement[]>([]);
const handleBranchChange = (newBranch: number) => {
setCurrentBranch(newBranch);
onBranchChange?.(newBranch);
};
const goToPrevious = () => {
const newBranch =
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
handleBranchChange(newBranch);
};
const goToNext = () => {
const newBranch =
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
handleBranchChange(newBranch);
};
const contextValue: MessageBranchContextType = {
currentBranch,
totalBranches: branches.length,
goToPrevious,
goToNext,
branches,
setBranches,
};
return (
<MessageBranchContext.Provider value={contextValue}>
<div
className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
{...props}
/>
</MessageBranchContext.Provider>
);
};
export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
export const MessageBranchContent = ({
children,
...props
}: MessageBranchContentProps) => {
const { currentBranch, setBranches, branches } = useMessageBranch();
const childrenArray = Array.isArray(children) ? children : [children];
// Use useEffect to update branches when they change
useEffect(() => {
if (branches.length !== childrenArray.length) {
setBranches(childrenArray);
}
}, [childrenArray, branches, setBranches]);
return childrenArray.map((branch, index) => (
<div
className={cn(
"grid gap-2 overflow-hidden [&>div]:pb-0",
index === currentBranch ? "block" : "hidden",
)}
key={branch.key}
{...props}
>
{branch}
</div>
));
};
export type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"];
};
export const MessageBranchSelector = ({
className,
from,
...props
}: MessageBranchSelectorProps) => {
const { totalBranches } = useMessageBranch();
// Don't render if there's only one branch
if (totalBranches <= 1) {
return null;
}
return (
<ButtonGroup
className="[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md"
orientation="horizontal"
{...props}
/>
);
};
export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
export const MessageBranchPrevious = ({
children,
...props
}: MessageBranchPreviousProps) => {
const { goToPrevious, totalBranches } = useMessageBranch();
return (
<Button
aria-label="Previous branch"
disabled={totalBranches <= 1}
onClick={goToPrevious}
size="icon-sm"
type="button"
variant="ghost"
{...props}
>
{children ?? <ChevronLeftIcon size={14} />}
</Button>
);
};
export type MessageBranchNextProps = ComponentProps<typeof Button>;
export const MessageBranchNext = ({
children,
className,
...props
}: MessageBranchNextProps) => {
const { goToNext, totalBranches } = useMessageBranch();
return (
<Button
aria-label="Next branch"
disabled={totalBranches <= 1}
onClick={goToNext}
size="icon-sm"
type="button"
variant="ghost"
{...props}
>
{children ?? <ChevronRightIcon size={14} />}
</Button>
);
};
export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
export const MessageBranchPage = ({
className,
...props
}: MessageBranchPageProps) => {
const { currentBranch, totalBranches } = useMessageBranch();
return (
<ButtonGroupText
className={cn(
"text-muted-foreground border-none bg-transparent shadow-none",
className,
)}
{...props}
>
{currentBranch + 1} of {totalBranches}
</ButtonGroupText>
);
};
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
export const MessageResponse = memo(
({ className, ...props }: MessageResponseProps) => (
<Streamdown
className={cn(
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
className,
)}
{...props}
/>
),
(prevProps, nextProps) => prevProps.children === nextProps.children,
);
MessageResponse.displayName = "MessageResponse";
export type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
data: FileUIPart;
className?: string;
onRemove?: () => void;
};
export function MessageAttachment({
data,
className,
onRemove,
...props
}: MessageAttachmentProps) {
const filename = data.filename || "";
const mediaType =
data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
const isImage = mediaType === "image";
const attachmentLabel = filename || (isImage ? "Image" : "Attachment");
return (
<div
className={cn(
"group relative size-24 overflow-hidden rounded-lg",
className,
)}
{...props}
>
{isImage ? (
<>
<img
alt={filename || "attachment"}
className="size-full object-cover"
height={100}
src={data.url}
width={100}
/>
{onRemove && (
<Button
aria-label="Remove attachment"
className="bg-background/80 hover:bg-background absolute top-2 right-2 size-6 rounded-full p-0 opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100 [&>svg]:size-3"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
type="button"
variant="ghost"
>
<XIcon />
<span className="sr-only">Remove</span>
</Button>
)}
</>
) : (
<>
<Tooltip>
<TooltipTrigger asChild>
<div className="bg-muted text-muted-foreground flex size-full shrink-0 items-center justify-center rounded-lg">
<PaperclipIcon className="size-4" />
</div>
</TooltipTrigger>
<TooltipContent>
<p>{attachmentLabel}</p>
</TooltipContent>
</Tooltip>
{onRemove && (
<Button
aria-label="Remove attachment"
className="hover:bg-accent size-6 shrink-0 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100 [&>svg]:size-3"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
type="button"
variant="ghost"
>
<XIcon />
<span className="sr-only">Remove</span>
</Button>
)}
</>
)}
</div>
);
}
export type MessageAttachmentsProps = ComponentProps<"div">;
export function MessageAttachments({
children,
className,
...props
}: MessageAttachmentsProps) {
if (!children) {
return null;
}
return (
<div
className={cn(
"ml-auto flex w-fit flex-wrap items-start gap-2",
className,
)}
{...props}
>
{children}
</div>
);
}
export type MessageToolbarProps = ComponentProps<"div">;
export const MessageToolbar = ({
className,
children,
...props
}: MessageToolbarProps) => (
<div
className={cn(
"mt-4 flex w-full items-center justify-between gap-4",
className,
)}
{...props}
>
{children}
</div>
);
@@ -1,208 +1,208 @@
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import type { ComponentProps, ReactNode } from "react";
export type ModelSelectorProps = ComponentProps<typeof Dialog>;
export const ModelSelector = (props: ModelSelectorProps) => (
<Dialog {...props} />
);
export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>;
export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
<DialogTrigger {...props} />
);
export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
title?: ReactNode;
};
export const ModelSelectorContent = ({
className,
children,
title = "Model Selector",
...props
}: ModelSelectorContentProps) => (
<DialogContent className={cn("p-0", className)} {...props}>
<DialogTitle className="sr-only">{title}</DialogTitle>
<Command className="**:data-[slot=command-input-wrapper]:h-auto">
{children}
</Command>
</DialogContent>
);
export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>;
export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
<CommandDialog {...props} />
);
export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>;
export const ModelSelectorInput = ({
className,
...props
}: ModelSelectorInputProps) => (
<CommandInput className={cn("h-auto py-3.5", className)} {...props} />
);
export type ModelSelectorListProps = ComponentProps<typeof CommandList>;
export const ModelSelectorList = (props: ModelSelectorListProps) => (
<CommandList {...props} />
);
export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>;
export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
<CommandEmpty {...props} />
);
export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>;
export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
<CommandGroup {...props} />
);
export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>;
export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
<CommandItem {...props} />
);
export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>;
export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
<CommandShortcut {...props} />
);
export type ModelSelectorSeparatorProps = ComponentProps<
typeof CommandSeparator
>;
export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
<CommandSeparator {...props} />
);
export type ModelSelectorLogoProps = Omit<
ComponentProps<"img">,
"src" | "alt"
> & {
provider:
| "moonshotai-cn"
| "lucidquery"
| "moonshotai"
| "zai-coding-plan"
| "alibaba"
| "xai"
| "vultr"
| "nvidia"
| "upstage"
| "groq"
| "github-copilot"
| "mistral"
| "vercel"
| "nebius"
| "deepseek"
| "alibaba-cn"
| "google-vertex-anthropic"
| "venice"
| "chutes"
| "cortecs"
| "github-models"
| "togetherai"
| "azure"
| "baseten"
| "huggingface"
| "opencode"
| "fastrouter"
| "google"
| "google-vertex"
| "cloudflare-workers-ai"
| "inception"
| "wandb"
| "openai"
| "zhipuai-coding-plan"
| "perplexity"
| "openrouter"
| "zenmux"
| "v0"
| "iflowcn"
| "synthetic"
| "deepinfra"
| "zhipuai"
| "submodel"
| "zai"
| "inference"
| "requesty"
| "morph"
| "lmstudio"
| "anthropic"
| "aihubmix"
| "fireworks-ai"
| "modelscope"
| "llama"
| "scaleway"
| "amazon-bedrock"
| "cerebras"
| (string & {});
};
export const ModelSelectorLogo = ({
provider,
className,
...props
}: ModelSelectorLogoProps) => (
<img
{...props}
alt={`${provider} logo`}
className={cn("size-3 dark:invert", className)}
height={12}
src={`https://models.dev/logos/${provider}.svg`}
width={12}
/>
);
export type ModelSelectorLogoGroupProps = ComponentProps<"div">;
export const ModelSelectorLogoGroup = ({
className,
...props
}: ModelSelectorLogoGroupProps) => (
<div
className={cn(
"[&>img]:bg-background dark:[&>img]:bg-foreground flex shrink-0 items-center -space-x-1 [&>img]:rounded-full [&>img]:p-px [&>img]:ring-1",
className,
)}
{...props}
/>
);
export type ModelSelectorNameProps = ComponentProps<"span">;
export const ModelSelectorName = ({
className,
...props
}: ModelSelectorNameProps) => (
<span
className={cn("flex-1 truncate text-left text-xs", className)}
{...props}
/>
);
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import type { ComponentProps, ReactNode } from "react";
export type ModelSelectorProps = ComponentProps<typeof Dialog>;
export const ModelSelector = (props: ModelSelectorProps) => (
<Dialog {...props} />
);
export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>;
export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
<DialogTrigger {...props} />
);
export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
title?: ReactNode;
};
export const ModelSelectorContent = ({
className,
children,
title = "Model Selector",
...props
}: ModelSelectorContentProps) => (
<DialogContent className={cn("p-0", className)} {...props}>
<DialogTitle className="sr-only">{title}</DialogTitle>
<Command className="**:data-[slot=command-input-wrapper]:h-auto">
{children}
</Command>
</DialogContent>
);
export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>;
export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
<CommandDialog {...props} />
);
export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>;
export const ModelSelectorInput = ({
className,
...props
}: ModelSelectorInputProps) => (
<CommandInput className={cn("h-auto py-3.5", className)} {...props} />
);
export type ModelSelectorListProps = ComponentProps<typeof CommandList>;
export const ModelSelectorList = (props: ModelSelectorListProps) => (
<CommandList {...props} />
);
export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>;
export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
<CommandEmpty {...props} />
);
export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>;
export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
<CommandGroup {...props} />
);
export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>;
export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
<CommandItem {...props} />
);
export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>;
export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
<CommandShortcut {...props} />
);
export type ModelSelectorSeparatorProps = ComponentProps<
typeof CommandSeparator
>;
export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
<CommandSeparator {...props} />
);
export type ModelSelectorLogoProps = Omit<
ComponentProps<"img">,
"src" | "alt"
> & {
provider:
| "moonshotai-cn"
| "lucidquery"
| "moonshotai"
| "zai-coding-plan"
| "alibaba"
| "xai"
| "vultr"
| "nvidia"
| "upstage"
| "groq"
| "github-copilot"
| "mistral"
| "vercel"
| "nebius"
| "deepseek"
| "alibaba-cn"
| "google-vertex-anthropic"
| "venice"
| "chutes"
| "cortecs"
| "github-models"
| "togetherai"
| "azure"
| "baseten"
| "huggingface"
| "opencode"
| "fastrouter"
| "google"
| "google-vertex"
| "cloudflare-workers-ai"
| "inception"
| "wandb"
| "openai"
| "zhipuai-coding-plan"
| "perplexity"
| "openrouter"
| "zenmux"
| "v0"
| "iflowcn"
| "synthetic"
| "deepinfra"
| "zhipuai"
| "submodel"
| "zai"
| "inference"
| "requesty"
| "morph"
| "lmstudio"
| "anthropic"
| "aihubmix"
| "fireworks-ai"
| "modelscope"
| "llama"
| "scaleway"
| "amazon-bedrock"
| "cerebras"
| (string & {});
};
export const ModelSelectorLogo = ({
provider,
className,
...props
}: ModelSelectorLogoProps) => (
<img
{...props}
alt={`${provider} logo`}
className={cn("size-3 dark:invert", className)}
height={12}
src={`https://models.dev/logos/${provider}.svg`}
width={12}
/>
);
export type ModelSelectorLogoGroupProps = ComponentProps<"div">;
export const ModelSelectorLogoGroup = ({
className,
...props
}: ModelSelectorLogoGroupProps) => (
<div
className={cn(
"[&>img]:bg-background dark:[&>img]:bg-foreground flex shrink-0 items-center -space-x-1 [&>img]:rounded-full [&>img]:p-px [&>img]:ring-1",
className,
)}
{...props}
/>
);
export type ModelSelectorNameProps = ComponentProps<"span">;
export const ModelSelectorName = ({
className,
...props
}: ModelSelectorNameProps) => (
<span
className={cn("flex-1 truncate text-left text-xs", className)}
{...props}
/>
);
@@ -1,71 +1,71 @@
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { Handle, Position } from "@xyflow/react";
import type { ComponentProps } from "react";
export type NodeProps = ComponentProps<typeof Card> & {
handles: {
target: boolean;
source: boolean;
};
};
export const Node = ({ handles, className, ...props }: NodeProps) => (
<Card
className={cn(
"node-container relative size-full h-auto w-sm gap-0 rounded-md p-0",
className,
)}
{...props}
>
{handles.target && <Handle position={Position.Left} type="target" />}
{handles.source && <Handle position={Position.Right} type="source" />}
{props.children}
</Card>
);
export type NodeHeaderProps = ComponentProps<typeof CardHeader>;
export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
<CardHeader
className={cn("bg-secondary gap-0.5 rounded-t-md border-b p-3!", className)}
{...props}
/>
);
export type NodeTitleProps = ComponentProps<typeof CardTitle>;
export const NodeTitle = (props: NodeTitleProps) => <CardTitle {...props} />;
export type NodeDescriptionProps = ComponentProps<typeof CardDescription>;
export const NodeDescription = (props: NodeDescriptionProps) => (
<CardDescription {...props} />
);
export type NodeActionProps = ComponentProps<typeof CardAction>;
export const NodeAction = (props: NodeActionProps) => <CardAction {...props} />;
export type NodeContentProps = ComponentProps<typeof CardContent>;
export const NodeContent = ({ className, ...props }: NodeContentProps) => (
<CardContent className={cn("p-3", className)} {...props} />
);
export type NodeFooterProps = ComponentProps<typeof CardFooter>;
export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
<CardFooter
className={cn("bg-secondary rounded-b-md border-t p-3!", className)}
{...props}
/>
);
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { Handle, Position } from "@xyflow/react";
import type { ComponentProps } from "react";
export type NodeProps = ComponentProps<typeof Card> & {
handles: {
target: boolean;
source: boolean;
};
};
export const Node = ({ handles, className, ...props }: NodeProps) => (
<Card
className={cn(
"node-container relative size-full h-auto w-sm gap-0 rounded-md p-0",
className,
)}
{...props}
>
{handles.target && <Handle position={Position.Left} type="target" />}
{handles.source && <Handle position={Position.Right} type="source" />}
{props.children}
</Card>
);
export type NodeHeaderProps = ComponentProps<typeof CardHeader>;
export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
<CardHeader
className={cn("bg-secondary gap-0.5 rounded-t-md border-b p-3!", className)}
{...props}
/>
);
export type NodeTitleProps = ComponentProps<typeof CardTitle>;
export const NodeTitle = (props: NodeTitleProps) => <CardTitle {...props} />;
export type NodeDescriptionProps = ComponentProps<typeof CardDescription>;
export const NodeDescription = (props: NodeDescriptionProps) => (
<CardDescription {...props} />
);
export type NodeActionProps = ComponentProps<typeof CardAction>;
export const NodeAction = (props: NodeActionProps) => <CardAction {...props} />;
export type NodeContentProps = ComponentProps<typeof CardContent>;
export const NodeContent = ({ className, ...props }: NodeContentProps) => (
<CardContent className={cn("p-3", className)} {...props} />
);
export type NodeFooterProps = ComponentProps<typeof CardFooter>;
export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
<CardFooter
className={cn("bg-secondary rounded-b-md border-t p-3!", className)}
{...props}
/>
);
@@ -1,365 +1,365 @@
"use client";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import {
ChevronDownIcon,
ExternalLinkIcon,
MessageCircleIcon,
} from "lucide-react";
import { type ComponentProps, createContext, useContext } from "react";
const providers = {
github: {
title: "Open in GitHub",
createUrl: (url: string) => url,
icon: (
<svg fill="currentColor" role="img" viewBox="0 0 24 24">
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
),
},
scira: {
title: "Open in Scira",
createUrl: (q: string) =>
`https://scira.ai/?${new URLSearchParams({
q,
})}`,
icon: (
<svg
fill="none"
height="934"
viewBox="0 0 910 934"
width="910"
xmlns="http://www.w3.org/2000/svg"
>
<title>Scira AI</title>
<path
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="20"
/>
<path
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="20"
/>
<path
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="30"
/>
</svg>
),
},
chatgpt: {
title: "Open in ChatGPT",
createUrl: (prompt: string) =>
`https://chatgpt.com/?${new URLSearchParams({
hints: "search",
prompt,
})}`,
icon: (
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>OpenAI</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
),
},
claude: {
title: "Open in Claude",
createUrl: (q: string) =>
`https://claude.ai/new?${new URLSearchParams({
q,
})}`,
icon: (
<svg
fill="currentColor"
role="img"
viewBox="0 0 12 12"
xmlns="http://www.w3.org/2000/svg"
>
<title>Claude</title>
<path
clipRule="evenodd"
d="M2.3545 7.9775L4.7145 6.654L4.7545 6.539L4.7145 6.475H4.6L4.205 6.451L2.856 6.4145L1.6865 6.366L0.5535 6.305L0.268 6.2445L0 5.892L0.0275 5.716L0.2675 5.5555L0.6105 5.5855L1.3705 5.637L2.5095 5.716L3.3355 5.7645L4.56 5.892H4.7545L4.782 5.8135L4.715 5.7645L4.6635 5.716L3.4845 4.918L2.2085 4.074L1.5405 3.588L1.1785 3.3425L0.9965 3.1115L0.9175 2.6075L1.2455 2.2465L1.686 2.2765L1.7985 2.307L2.245 2.65L3.199 3.388L4.4445 4.3045L4.627 4.4565L4.6995 4.405L4.709 4.3685L4.627 4.2315L3.9495 3.0085L3.2265 1.7635L2.9045 1.2475L2.8195 0.938C2.78711 0.819128 2.76965 0.696687 2.7675 0.5735L3.1415 0.067L3.348 0L3.846 0.067L4.056 0.249L4.366 0.956L4.867 2.0705L5.6445 3.5855L5.8725 4.0345L5.994 4.4505L6.0395 4.578H6.1185V4.505L6.1825 3.652L6.301 2.6045L6.416 1.257L6.456 0.877L6.644 0.422L7.0175 0.176L7.3095 0.316L7.5495 0.6585L7.516 0.8805L7.373 1.806L7.0935 3.2575L6.9115 4.2285H7.0175L7.139 4.1075L7.6315 3.4545L8.4575 2.4225L8.8225 2.0125L9.2475 1.5605L9.521 1.345H10.0375L10.4175 1.9095L10.2475 2.4925L9.7155 3.166L9.275 3.737L8.643 4.587L8.248 5.267L8.2845 5.322L8.3785 5.312L9.8065 5.009L10.578 4.869L11.4985 4.7115L11.915 4.9055L11.9605 5.103L11.7965 5.5065L10.812 5.7495L9.6575 5.9805L7.938 6.387L7.917 6.402L7.9415 6.4325L8.716 6.5055L9.047 6.5235H9.858L11.368 6.636L11.763 6.897L12 7.216L11.9605 7.4585L11.353 7.7685L10.533 7.574L8.6185 7.119L7.9625 6.9545H7.8715V7.0095L8.418 7.5435L9.421 8.4485L10.6755 9.6135L10.739 9.9025L10.578 10.13L10.408 10.1055L9.3055 9.277L8.88 8.9035L7.917 8.0935H7.853V8.1785L8.075 8.503L9.2475 10.2635L9.3085 10.8035L9.2235 10.98L8.9195 11.0865L8.5855 11.0255L7.8985 10.063L7.191 8.9795L6.6195 8.008L6.5495 8.048L6.2125 11.675L6.0545 11.86L5.69 12L5.3865 11.7695L5.2255 11.396L5.3865 10.658L5.581 9.696L5.7385 8.931L5.8815 7.981L5.9665 7.665L5.9605 7.644L5.8905 7.653L5.1735 8.6365L4.0835 10.109L3.2205 11.0315L3.0135 11.1135L2.655 10.9285L2.6885 10.5975L2.889 10.303L4.083 8.785L4.803 7.844L5.268 7.301L5.265 7.222H5.2375L2.066 9.28L1.501 9.353L1.2575 9.125L1.288 8.752L1.4035 8.6305L2.3575 7.9745L2.3545 7.9775Z"
fillRule="evenodd"
/>
</svg>
),
},
t3: {
title: "Open in T3 Chat",
createUrl: (q: string) =>
`https://t3.chat/new?${new URLSearchParams({
q,
})}`,
icon: <MessageCircleIcon />,
},
v0: {
title: "Open in v0",
createUrl: (q: string) =>
`https://v0.app?${new URLSearchParams({
q,
})}`,
icon: (
<svg
fill="currentColor"
viewBox="0 0 147 70"
xmlns="http://www.w3.org/2000/svg"
>
<title>v0</title>
<path d="M56 50.2031V14H70V60.1562C70 65.5928 65.5928 70 60.1562 70C57.5605 70 54.9982 68.9992 53.1562 67.1573L0 14H19.7969L56 50.2031Z" />
<path d="M147 56H133V23.9531L100.953 56H133V70H96.6875C85.8144 70 77 61.1856 77 50.3125V14H91V46.1562L123.156 14H91V0H127.312C138.186 0 147 8.81439 147 19.6875V56Z" />
</svg>
),
},
cursor: {
title: "Open in Cursor",
createUrl: (text: string) => {
const url = new URL("https://cursor.com/link/prompt");
url.searchParams.set("text", text);
return url.toString();
},
icon: (
<svg
version="1.1"
viewBox="0 0 466.73 532.09"
xmlns="http://www.w3.org/2000/svg"
>
<title>Cursor</title>
<path
d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"
fill="currentColor"
/>
</svg>
),
},
};
const OpenInContext = createContext<{ query: string } | undefined>(undefined);
const useOpenInContext = () => {
const context = useContext(OpenInContext);
if (!context) {
throw new Error("OpenIn components must be used within an OpenIn provider");
}
return context;
};
export type OpenInProps = ComponentProps<typeof DropdownMenu> & {
query: string;
};
export const OpenIn = ({ query, ...props }: OpenInProps) => (
<OpenInContext.Provider value={{ query }}>
<DropdownMenu {...props} />
</OpenInContext.Provider>
);
export type OpenInContentProps = ComponentProps<typeof DropdownMenuContent>;
export const OpenInContent = ({ className, ...props }: OpenInContentProps) => (
<DropdownMenuContent
align="start"
className={cn("w-[240px]", className)}
{...props}
/>
);
export type OpenInItemProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInItem = (props: OpenInItemProps) => (
<DropdownMenuItem {...props} />
);
export type OpenInLabelProps = ComponentProps<typeof DropdownMenuLabel>;
export const OpenInLabel = (props: OpenInLabelProps) => (
<DropdownMenuLabel {...props} />
);
export type OpenInSeparatorProps = ComponentProps<typeof DropdownMenuSeparator>;
export const OpenInSeparator = (props: OpenInSeparatorProps) => (
<DropdownMenuSeparator {...props} />
);
export type OpenInTriggerProps = ComponentProps<typeof DropdownMenuTrigger>;
export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => (
<DropdownMenuTrigger {...props} asChild>
{children ?? (
<Button type="button" variant="outline">
Open in chat
<ChevronDownIcon className="size-4" />
</Button>
)}
</DropdownMenuTrigger>
);
export type OpenInChatGPTProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInChatGPT = (props: OpenInChatGPTProps) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.chatgpt.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.chatgpt.icon}</span>
<span className="flex-1">{providers.chatgpt.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
export type OpenInClaudeProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInClaude = (props: OpenInClaudeProps) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.claude.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.claude.icon}</span>
<span className="flex-1">{providers.claude.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
export type OpenInT3Props = ComponentProps<typeof DropdownMenuItem>;
export const OpenInT3 = (props: OpenInT3Props) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.t3.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.t3.icon}</span>
<span className="flex-1">{providers.t3.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
export type OpenInSciraProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInScira = (props: OpenInSciraProps) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.scira.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.scira.icon}</span>
<span className="flex-1">{providers.scira.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
export type OpenInv0Props = ComponentProps<typeof DropdownMenuItem>;
export const OpenInv0 = (props: OpenInv0Props) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.v0.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.v0.icon}</span>
<span className="flex-1">{providers.v0.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
export type OpenInCursorProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInCursor = (props: OpenInCursorProps) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.cursor.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.cursor.icon}</span>
<span className="flex-1">{providers.cursor.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
"use client";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import {
ChevronDownIcon,
ExternalLinkIcon,
MessageCircleIcon,
} from "lucide-react";
import { type ComponentProps, createContext, useContext } from "react";
const providers = {
github: {
title: "Open in GitHub",
createUrl: (url: string) => url,
icon: (
<svg fill="currentColor" role="img" viewBox="0 0 24 24">
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
),
},
scira: {
title: "Open in Scira",
createUrl: (q: string) =>
`https://scira.ai/?${new URLSearchParams({
q,
})}`,
icon: (
<svg
fill="none"
height="934"
viewBox="0 0 910 934"
width="910"
xmlns="http://www.w3.org/2000/svg"
>
<title>Scira AI</title>
<path
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="20"
/>
<path
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="20"
/>
<path
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
fill="currentColor"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="8"
/>
<path
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="30"
/>
</svg>
),
},
chatgpt: {
title: "Open in ChatGPT",
createUrl: (prompt: string) =>
`https://chatgpt.com/?${new URLSearchParams({
hints: "search",
prompt,
})}`,
icon: (
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>OpenAI</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
),
},
claude: {
title: "Open in Claude",
createUrl: (q: string) =>
`https://claude.ai/new?${new URLSearchParams({
q,
})}`,
icon: (
<svg
fill="currentColor"
role="img"
viewBox="0 0 12 12"
xmlns="http://www.w3.org/2000/svg"
>
<title>Claude</title>
<path
clipRule="evenodd"
d="M2.3545 7.9775L4.7145 6.654L4.7545 6.539L4.7145 6.475H4.6L4.205 6.451L2.856 6.4145L1.6865 6.366L0.5535 6.305L0.268 6.2445L0 5.892L0.0275 5.716L0.2675 5.5555L0.6105 5.5855L1.3705 5.637L2.5095 5.716L3.3355 5.7645L4.56 5.892H4.7545L4.782 5.8135L4.715 5.7645L4.6635 5.716L3.4845 4.918L2.2085 4.074L1.5405 3.588L1.1785 3.3425L0.9965 3.1115L0.9175 2.6075L1.2455 2.2465L1.686 2.2765L1.7985 2.307L2.245 2.65L3.199 3.388L4.4445 4.3045L4.627 4.4565L4.6995 4.405L4.709 4.3685L4.627 4.2315L3.9495 3.0085L3.2265 1.7635L2.9045 1.2475L2.8195 0.938C2.78711 0.819128 2.76965 0.696687 2.7675 0.5735L3.1415 0.067L3.348 0L3.846 0.067L4.056 0.249L4.366 0.956L4.867 2.0705L5.6445 3.5855L5.8725 4.0345L5.994 4.4505L6.0395 4.578H6.1185V4.505L6.1825 3.652L6.301 2.6045L6.416 1.257L6.456 0.877L6.644 0.422L7.0175 0.176L7.3095 0.316L7.5495 0.6585L7.516 0.8805L7.373 1.806L7.0935 3.2575L6.9115 4.2285H7.0175L7.139 4.1075L7.6315 3.4545L8.4575 2.4225L8.8225 2.0125L9.2475 1.5605L9.521 1.345H10.0375L10.4175 1.9095L10.2475 2.4925L9.7155 3.166L9.275 3.737L8.643 4.587L8.248 5.267L8.2845 5.322L8.3785 5.312L9.8065 5.009L10.578 4.869L11.4985 4.7115L11.915 4.9055L11.9605 5.103L11.7965 5.5065L10.812 5.7495L9.6575 5.9805L7.938 6.387L7.917 6.402L7.9415 6.4325L8.716 6.5055L9.047 6.5235H9.858L11.368 6.636L11.763 6.897L12 7.216L11.9605 7.4585L11.353 7.7685L10.533 7.574L8.6185 7.119L7.9625 6.9545H7.8715V7.0095L8.418 7.5435L9.421 8.4485L10.6755 9.6135L10.739 9.9025L10.578 10.13L10.408 10.1055L9.3055 9.277L8.88 8.9035L7.917 8.0935H7.853V8.1785L8.075 8.503L9.2475 10.2635L9.3085 10.8035L9.2235 10.98L8.9195 11.0865L8.5855 11.0255L7.8985 10.063L7.191 8.9795L6.6195 8.008L6.5495 8.048L6.2125 11.675L6.0545 11.86L5.69 12L5.3865 11.7695L5.2255 11.396L5.3865 10.658L5.581 9.696L5.7385 8.931L5.8815 7.981L5.9665 7.665L5.9605 7.644L5.8905 7.653L5.1735 8.6365L4.0835 10.109L3.2205 11.0315L3.0135 11.1135L2.655 10.9285L2.6885 10.5975L2.889 10.303L4.083 8.785L4.803 7.844L5.268 7.301L5.265 7.222H5.2375L2.066 9.28L1.501 9.353L1.2575 9.125L1.288 8.752L1.4035 8.6305L2.3575 7.9745L2.3545 7.9775Z"
fillRule="evenodd"
/>
</svg>
),
},
t3: {
title: "Open in T3 Chat",
createUrl: (q: string) =>
`https://t3.chat/new?${new URLSearchParams({
q,
})}`,
icon: <MessageCircleIcon />,
},
v0: {
title: "Open in v0",
createUrl: (q: string) =>
`https://v0.app?${new URLSearchParams({
q,
})}`,
icon: (
<svg
fill="currentColor"
viewBox="0 0 147 70"
xmlns="http://www.w3.org/2000/svg"
>
<title>v0</title>
<path d="M56 50.2031V14H70V60.1562C70 65.5928 65.5928 70 60.1562 70C57.5605 70 54.9982 68.9992 53.1562 67.1573L0 14H19.7969L56 50.2031Z" />
<path d="M147 56H133V23.9531L100.953 56H133V70H96.6875C85.8144 70 77 61.1856 77 50.3125V14H91V46.1562L123.156 14H91V0H127.312C138.186 0 147 8.81439 147 19.6875V56Z" />
</svg>
),
},
cursor: {
title: "Open in Cursor",
createUrl: (text: string) => {
const url = new URL("https://cursor.com/link/prompt");
url.searchParams.set("text", text);
return url.toString();
},
icon: (
<svg
version="1.1"
viewBox="0 0 466.73 532.09"
xmlns="http://www.w3.org/2000/svg"
>
<title>Cursor</title>
<path
d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"
fill="currentColor"
/>
</svg>
),
},
};
const OpenInContext = createContext<{ query: string } | undefined>(undefined);
const useOpenInContext = () => {
const context = useContext(OpenInContext);
if (!context) {
throw new Error("OpenIn components must be used within an OpenIn provider");
}
return context;
};
export type OpenInProps = ComponentProps<typeof DropdownMenu> & {
query: string;
};
export const OpenIn = ({ query, ...props }: OpenInProps) => (
<OpenInContext.Provider value={{ query }}>
<DropdownMenu {...props} />
</OpenInContext.Provider>
);
export type OpenInContentProps = ComponentProps<typeof DropdownMenuContent>;
export const OpenInContent = ({ className, ...props }: OpenInContentProps) => (
<DropdownMenuContent
align="start"
className={cn("w-[240px]", className)}
{...props}
/>
);
export type OpenInItemProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInItem = (props: OpenInItemProps) => (
<DropdownMenuItem {...props} />
);
export type OpenInLabelProps = ComponentProps<typeof DropdownMenuLabel>;
export const OpenInLabel = (props: OpenInLabelProps) => (
<DropdownMenuLabel {...props} />
);
export type OpenInSeparatorProps = ComponentProps<typeof DropdownMenuSeparator>;
export const OpenInSeparator = (props: OpenInSeparatorProps) => (
<DropdownMenuSeparator {...props} />
);
export type OpenInTriggerProps = ComponentProps<typeof DropdownMenuTrigger>;
export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => (
<DropdownMenuTrigger {...props} asChild>
{children ?? (
<Button type="button" variant="outline">
Open in chat
<ChevronDownIcon className="size-4" />
</Button>
)}
</DropdownMenuTrigger>
);
export type OpenInChatGPTProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInChatGPT = (props: OpenInChatGPTProps) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.chatgpt.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.chatgpt.icon}</span>
<span className="flex-1">{providers.chatgpt.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
export type OpenInClaudeProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInClaude = (props: OpenInClaudeProps) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.claude.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.claude.icon}</span>
<span className="flex-1">{providers.claude.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
export type OpenInT3Props = ComponentProps<typeof DropdownMenuItem>;
export const OpenInT3 = (props: OpenInT3Props) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.t3.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.t3.icon}</span>
<span className="flex-1">{providers.t3.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
export type OpenInSciraProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInScira = (props: OpenInSciraProps) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.scira.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.scira.icon}</span>
<span className="flex-1">{providers.scira.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
export type OpenInv0Props = ComponentProps<typeof DropdownMenuItem>;
export const OpenInv0 = (props: OpenInv0Props) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.v0.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.v0.icon}</span>
<span className="flex-1">{providers.v0.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
export type OpenInCursorProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInCursor = (props: OpenInCursorProps) => {
const { query } = useOpenInContext();
return (
<DropdownMenuItem asChild {...props}>
<a
className="flex items-center gap-2"
href={providers.cursor.createUrl(query)}
rel="noopener noreferrer"
target="_blank"
>
<span className="shrink-0">{providers.cursor.icon}</span>
<span className="flex-1">{providers.cursor.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
};
@@ -1,15 +1,15 @@
import { cn } from "@/lib/utils";
import { Panel as PanelPrimitive } from "@xyflow/react";
import type { ComponentProps } from "react";
type PanelProps = ComponentProps<typeof PanelPrimitive>;
export const Panel = ({ className, ...props }: PanelProps) => (
<PanelPrimitive
className={cn(
"bg-card m-4 overflow-hidden rounded-md border p-1",
className,
)}
{...props}
/>
);
import { cn } from "@/lib/utils";
import { Panel as PanelPrimitive } from "@xyflow/react";
import type { ComponentProps } from "react";
type PanelProps = ComponentProps<typeof PanelPrimitive>;
export const Panel = ({ className, ...props }: PanelProps) => (
<PanelPrimitive
className={cn(
"bg-card m-4 overflow-hidden rounded-md border p-1",
className,
)}
{...props}
/>
);
+142 -142
View File
@@ -1,142 +1,142 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { ChevronsUpDownIcon } from "lucide-react";
import type { ComponentProps } from "react";
import { createContext, useContext } from "react";
import { Shimmer } from "./shimmer";
type PlanContextValue = {
isStreaming: boolean;
};
const PlanContext = createContext<PlanContextValue | null>(null);
const usePlan = () => {
const context = useContext(PlanContext);
if (!context) {
throw new Error("Plan components must be used within Plan");
}
return context;
};
export type PlanProps = ComponentProps<typeof Collapsible> & {
isStreaming?: boolean;
};
export const Plan = ({
className,
isStreaming = false,
children,
...props
}: PlanProps) => (
<PlanContext.Provider value={{ isStreaming }}>
<Collapsible asChild data-slot="plan" {...props}>
<Card className={cn("shadow-none", className)}>{children}</Card>
</Collapsible>
</PlanContext.Provider>
);
export type PlanHeaderProps = ComponentProps<typeof CardHeader>;
export const PlanHeader = ({ className, ...props }: PlanHeaderProps) => (
<CardHeader
className={cn("flex items-start justify-between", className)}
data-slot="plan-header"
{...props}
/>
);
export type PlanTitleProps = Omit<
ComponentProps<typeof CardTitle>,
"children"
> & {
children: string;
};
export const PlanTitle = ({ children, ...props }: PlanTitleProps) => {
const { isStreaming } = usePlan();
return (
<CardTitle data-slot="plan-title" {...props}>
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
</CardTitle>
);
};
export type PlanDescriptionProps = Omit<
ComponentProps<typeof CardDescription>,
"children"
> & {
children: string;
};
export const PlanDescription = ({
className,
children,
...props
}: PlanDescriptionProps) => {
const { isStreaming } = usePlan();
return (
<CardDescription
className={cn("text-balance", className)}
data-slot="plan-description"
{...props}
>
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
</CardDescription>
);
};
export type PlanActionProps = ComponentProps<typeof CardAction>;
export const PlanAction = (props: PlanActionProps) => (
<CardAction data-slot="plan-action" {...props} />
);
export type PlanContentProps = ComponentProps<typeof CardContent>;
export const PlanContent = (props: PlanContentProps) => (
<CollapsibleContent asChild>
<CardContent data-slot="plan-content" {...props} />
</CollapsibleContent>
);
export type PlanFooterProps = ComponentProps<"div">;
export const PlanFooter = (props: PlanFooterProps) => (
<CardFooter data-slot="plan-footer" {...props} />
);
export type PlanTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => (
<CollapsibleTrigger asChild>
<Button
className={cn("size-8", className)}
data-slot="plan-trigger"
size="icon"
variant="ghost"
{...props}
>
<ChevronsUpDownIcon className="size-4" />
<span className="sr-only">Toggle plan</span>
</Button>
</CollapsibleTrigger>
);
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { ChevronsUpDownIcon } from "lucide-react";
import type { ComponentProps } from "react";
import { createContext, useContext } from "react";
import { Shimmer } from "./shimmer";
type PlanContextValue = {
isStreaming: boolean;
};
const PlanContext = createContext<PlanContextValue | null>(null);
const usePlan = () => {
const context = useContext(PlanContext);
if (!context) {
throw new Error("Plan components must be used within Plan");
}
return context;
};
export type PlanProps = ComponentProps<typeof Collapsible> & {
isStreaming?: boolean;
};
export const Plan = ({
className,
isStreaming = false,
children,
...props
}: PlanProps) => (
<PlanContext.Provider value={{ isStreaming }}>
<Collapsible asChild data-slot="plan" {...props}>
<Card className={cn("shadow-none", className)}>{children}</Card>
</Collapsible>
</PlanContext.Provider>
);
export type PlanHeaderProps = ComponentProps<typeof CardHeader>;
export const PlanHeader = ({ className, ...props }: PlanHeaderProps) => (
<CardHeader
className={cn("flex items-start justify-between", className)}
data-slot="plan-header"
{...props}
/>
);
export type PlanTitleProps = Omit<
ComponentProps<typeof CardTitle>,
"children"
> & {
children: string;
};
export const PlanTitle = ({ children, ...props }: PlanTitleProps) => {
const { isStreaming } = usePlan();
return (
<CardTitle data-slot="plan-title" {...props}>
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
</CardTitle>
);
};
export type PlanDescriptionProps = Omit<
ComponentProps<typeof CardDescription>,
"children"
> & {
children: string;
};
export const PlanDescription = ({
className,
children,
...props
}: PlanDescriptionProps) => {
const { isStreaming } = usePlan();
return (
<CardDescription
className={cn("text-balance", className)}
data-slot="plan-description"
{...props}
>
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
</CardDescription>
);
};
export type PlanActionProps = ComponentProps<typeof CardAction>;
export const PlanAction = (props: PlanActionProps) => (
<CardAction data-slot="plan-action" {...props} />
);
export type PlanContentProps = ComponentProps<typeof CardContent>;
export const PlanContent = (props: PlanContentProps) => (
<CollapsibleContent asChild>
<CardContent data-slot="plan-content" {...props} />
</CollapsibleContent>
);
export type PlanFooterProps = ComponentProps<"div">;
export const PlanFooter = (props: PlanFooterProps) => (
<CardFooter data-slot="plan-footer" {...props} />
);
export type PlanTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => (
<CollapsibleTrigger asChild>
<Button
className={cn("size-8", className)}
data-slot="plan-trigger"
size="icon"
variant="ghost"
{...props}
>
<ChevronsUpDownIcon className="size-4" />
<span className="sr-only">Toggle plan</span>
</Button>
</CollapsibleTrigger>
);
File diff suppressed because it is too large. Load diff
+274 -274
View File
@@ -1,274 +1,274 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { ChevronDownIcon, PaperclipIcon } from "lucide-react";
import type { ComponentProps } from "react";
export type QueueMessagePart = {
type: string;
text?: string;
url?: string;
filename?: string;
mediaType?: string;
};
export type QueueMessage = {
id: string;
parts: QueueMessagePart[];
};
export type QueueTodo = {
id: string;
title: string;
description?: string;
status?: "pending" | "completed";
};
export type QueueItemProps = ComponentProps<"li">;
export const QueueItem = ({ className, ...props }: QueueItemProps) => (
<li
className={cn(
"group hover:bg-muted flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors",
className,
)}
{...props}
/>
);
export type QueueItemIndicatorProps = ComponentProps<"span"> & {
completed?: boolean;
};
export const QueueItemIndicator = ({
completed = false,
className,
...props
}: QueueItemIndicatorProps) => (
<span
className={cn(
"mt-0.5 inline-block size-2.5 rounded-full border",
completed
? "border-muted-foreground/20 bg-muted-foreground/10"
: "border-muted-foreground/50",
className,
)}
{...props}
/>
);
export type QueueItemContentProps = ComponentProps<"span"> & {
completed?: boolean;
};
export const QueueItemContent = ({
completed = false,
className,
...props
}: QueueItemContentProps) => (
<span
className={cn(
"line-clamp-1 grow break-words",
completed
? "text-muted-foreground/50 line-through"
: "text-muted-foreground",
className,
)}
{...props}
/>
);
export type QueueItemDescriptionProps = ComponentProps<"div"> & {
completed?: boolean;
};
export const QueueItemDescription = ({
completed = false,
className,
...props
}: QueueItemDescriptionProps) => (
<div
className={cn(
"ml-6 text-xs",
completed
? "text-muted-foreground/40 line-through"
: "text-muted-foreground",
className,
)}
{...props}
/>
);
export type QueueItemActionsProps = ComponentProps<"div">;
export const QueueItemActions = ({
className,
...props
}: QueueItemActionsProps) => (
<div className={cn("flex gap-1", className)} {...props} />
);
export type QueueItemActionProps = Omit<
ComponentProps<typeof Button>,
"variant" | "size"
>;
export const QueueItemAction = ({
className,
...props
}: QueueItemActionProps) => (
<Button
className={cn(
"text-muted-foreground hover:bg-muted-foreground/10 hover:text-foreground size-auto rounded p-1 opacity-0 transition-opacity group-hover:opacity-100",
className,
)}
size="icon"
type="button"
variant="ghost"
{...props}
/>
);
export type QueueItemAttachmentProps = ComponentProps<"div">;
export const QueueItemAttachment = ({
className,
...props
}: QueueItemAttachmentProps) => (
<div className={cn("mt-1 flex flex-wrap gap-2", className)} {...props} />
);
export type QueueItemImageProps = ComponentProps<"img">;
export const QueueItemImage = ({
className,
...props
}: QueueItemImageProps) => (
<img
alt=""
className={cn("h-8 w-8 rounded border object-cover", className)}
height={32}
width={32}
{...props}
/>
);
export type QueueItemFileProps = ComponentProps<"span">;
export const QueueItemFile = ({
children,
className,
...props
}: QueueItemFileProps) => (
<span
className={cn(
"bg-muted flex items-center gap-1 rounded border px-2 py-1 text-xs",
className,
)}
{...props}
>
<PaperclipIcon size={12} />
<span className="max-w-[100px] truncate">{children}</span>
</span>
);
export type QueueListProps = ComponentProps<typeof ScrollArea>;
export const QueueList = ({
children,
className,
...props
}: QueueListProps) => (
<ScrollArea className={cn("mt-2 -mb-1", className)} {...props}>
<div className="max-h-40 pr-4">
<ul>{children}</ul>
</div>
</ScrollArea>
);
// QueueSection - collapsible section container
export type QueueSectionProps = ComponentProps<typeof Collapsible>;
export const QueueSection = ({
className,
defaultOpen = true,
...props
}: QueueSectionProps) => (
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
);
// QueueSectionTrigger - section header/trigger
export type QueueSectionTriggerProps = ComponentProps<"button">;
export const QueueSectionTrigger = ({
children,
className,
...props
}: QueueSectionTriggerProps) => (
<CollapsibleTrigger asChild>
<button
className={cn(
"group bg-muted/40 text-muted-foreground hover:bg-muted flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm font-medium transition-colors",
className,
)}
type="button"
{...props}
>
{children}
</button>
</CollapsibleTrigger>
);
// QueueSectionLabel - label content with icon and count
export type QueueSectionLabelProps = ComponentProps<"span"> & {
count?: number;
label: string;
icon?: React.ReactNode;
};
export const QueueSectionLabel = ({
count,
label,
icon,
className,
...props
}: QueueSectionLabelProps) => (
<span className={cn("flex items-center gap-2", className)} {...props}>
<ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" />
{icon}
<span>
{count} {label}
</span>
</span>
);
// QueueSectionContent - collapsible content area
export type QueueSectionContentProps = ComponentProps<
typeof CollapsibleContent
>;
export const QueueSectionContent = ({
className,
...props
}: QueueSectionContentProps) => (
<CollapsibleContent className={cn(className)} {...props} />
);
export type QueueProps = ComponentProps<"div">;
export const Queue = ({ className, ...props }: QueueProps) => (
<div
className={cn(
"border-border bg-background flex flex-col gap-2 rounded-xl border px-3 pt-2 pb-2 shadow-xs",
className,
)}
{...props}
/>
);
"use client";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { ChevronDownIcon, PaperclipIcon } from "lucide-react";
import type { ComponentProps } from "react";
export type QueueMessagePart = {
type: string;
text?: string;
url?: string;
filename?: string;
mediaType?: string;
};
export type QueueMessage = {
id: string;
parts: QueueMessagePart[];
};
export type QueueTodo = {
id: string;
title: string;
description?: string;
status?: "pending" | "completed";
};
export type QueueItemProps = ComponentProps<"li">;
export const QueueItem = ({ className, ...props }: QueueItemProps) => (
<li
className={cn(
"group hover:bg-muted flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors",
className,
)}
{...props}
/>
);
export type QueueItemIndicatorProps = ComponentProps<"span"> & {
completed?: boolean;
};
export const QueueItemIndicator = ({
completed = false,
className,
...props
}: QueueItemIndicatorProps) => (
<span
className={cn(
"mt-0.5 inline-block size-2.5 rounded-full border",
completed
? "border-muted-foreground/20 bg-muted-foreground/10"
: "border-muted-foreground/50",
className,
)}
{...props}
/>
);
export type QueueItemContentProps = ComponentProps<"span"> & {
completed?: boolean;
};
export const QueueItemContent = ({
completed = false,
className,
...props
}: QueueItemContentProps) => (
<span
className={cn(
"line-clamp-1 grow break-words",
completed
? "text-muted-foreground/50 line-through"
: "text-muted-foreground",
className,
)}
{...props}
/>
);
export type QueueItemDescriptionProps = ComponentProps<"div"> & {
completed?: boolean;
};
export const QueueItemDescription = ({
completed = false,
className,
...props
}: QueueItemDescriptionProps) => (
<div
className={cn(
"ml-6 text-xs",
completed
? "text-muted-foreground/40 line-through"
: "text-muted-foreground",
className,
)}
{...props}
/>
);
export type QueueItemActionsProps = ComponentProps<"div">;
export const QueueItemActions = ({
className,
...props
}: QueueItemActionsProps) => (
<div className={cn("flex gap-1", className)} {...props} />
);
export type QueueItemActionProps = Omit<
ComponentProps<typeof Button>,
"variant" | "size"
>;
export const QueueItemAction = ({
className,
...props
}: QueueItemActionProps) => (
<Button
className={cn(
"text-muted-foreground hover:bg-muted-foreground/10 hover:text-foreground size-auto rounded p-1 opacity-0 transition-opacity group-hover:opacity-100",
className,
)}
size="icon"
type="button"
variant="ghost"
{...props}
/>
);
export type QueueItemAttachmentProps = ComponentProps<"div">;
export const QueueItemAttachment = ({
className,
...props
}: QueueItemAttachmentProps) => (
<div className={cn("mt-1 flex flex-wrap gap-2", className)} {...props} />
);
export type QueueItemImageProps = ComponentProps<"img">;
export const QueueItemImage = ({
className,
...props
}: QueueItemImageProps) => (
<img
alt=""
className={cn("h-8 w-8 rounded border object-cover", className)}
height={32}
width={32}
{...props}
/>
);
export type QueueItemFileProps = ComponentProps<"span">;
export const QueueItemFile = ({
children,
className,
...props
}: QueueItemFileProps) => (
<span
className={cn(
"bg-muted flex items-center gap-1 rounded border px-2 py-1 text-xs",
className,
)}
{...props}
>
<PaperclipIcon size={12} />
<span className="max-w-[100px] truncate">{children}</span>
</span>
);
export type QueueListProps = ComponentProps<typeof ScrollArea>;
export const QueueList = ({
children,
className,
...props
}: QueueListProps) => (
<ScrollArea className={cn("mt-2 -mb-1", className)} {...props}>
<div className="max-h-40 pr-4">
<ul>{children}</ul>
</div>
</ScrollArea>
);
// QueueSection - collapsible section container
export type QueueSectionProps = ComponentProps<typeof Collapsible>;
export const QueueSection = ({
className,
defaultOpen = true,
...props
}: QueueSectionProps) => (
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
);
// QueueSectionTrigger - section header/trigger
export type QueueSectionTriggerProps = ComponentProps<"button">;
export const QueueSectionTrigger = ({
children,
className,
...props
}: QueueSectionTriggerProps) => (
<CollapsibleTrigger asChild>
<button
className={cn(
"group bg-muted/40 text-muted-foreground hover:bg-muted flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm font-medium transition-colors",
className,
)}
type="button"
{...props}
>
{children}
</button>
</CollapsibleTrigger>
);
// QueueSectionLabel - label content with icon and count
export type QueueSectionLabelProps = ComponentProps<"span"> & {
count?: number;
label: string;
icon?: React.ReactNode;
};
export const QueueSectionLabel = ({
count,
label,
icon,
className,
...props
}: QueueSectionLabelProps) => (
<span className={cn("flex items-center gap-2", className)} {...props}>
<ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" />
{icon}
<span>
{count} {label}
</span>
</span>
);
// QueueSectionContent - collapsible content area
export type QueueSectionContentProps = ComponentProps<
typeof CollapsibleContent
>;
export const QueueSectionContent = ({
className,
...props
}: QueueSectionContentProps) => (
<CollapsibleContent className={cn(className)} {...props} />
);
export type QueueProps = ComponentProps<"div">;
export const Queue = ({ className, ...props }: QueueProps) => (
<div
className={cn(
"border-border bg-background flex flex-col gap-2 rounded-xl border px-3 pt-2 pb-2 shadow-xs",
className,
)}
{...props}
/>
);
@@ -1,188 +1,188 @@
"use client";
import { useControllableState } from "@radix-ui/react-use-controllable-state";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { BrainIcon, ChevronDownIcon } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { createContext, memo, useContext, useEffect, useState } from "react";
import { Streamdown } from "streamdown";
import { reasoningPlugins } from "@/core/streamdown/plugins";
import { Shimmer } from "./shimmer";
type ReasoningContextValue = {
isStreaming: boolean;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
duration: number | undefined;
};
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
export const useReasoning = () => {
const context = useContext(ReasoningContext);
if (!context) {
throw new Error("Reasoning components must be used within Reasoning");
}
return context;
};
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
isStreaming?: boolean;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
duration?: number;
};
const AUTO_CLOSE_DELAY = 1000;
const MS_IN_S = 1000;
export const Reasoning = memo(
({
className,
isStreaming = false,
open,
defaultOpen = true,
onOpenChange,
duration: durationProp,
children,
...props
}: ReasoningProps) => {
const [isOpen, setIsOpen] = useControllableState({
prop: open,
defaultProp: defaultOpen,
onChange: onOpenChange,
});
const [duration, setDuration] = useControllableState({
prop: durationProp,
defaultProp: undefined,
});
const [hasAutoClosed, setHasAutoClosed] = useState(false);
const [startTime, setStartTime] = useState<number | null>(null);
// Track duration when streaming starts and ends
useEffect(() => {
if (isStreaming) {
if (startTime === null) {
setStartTime(Date.now());
}
} else if (startTime !== null) {
setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S));
setStartTime(null);
}
}, [isStreaming, startTime, setDuration]);
// Auto-open when streaming starts, auto-close when streaming ends (once only)
useEffect(() => {
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
// Add a small delay before closing to allow user to see the content
const timer = setTimeout(() => {
setIsOpen(false);
setHasAutoClosed(true);
}, AUTO_CLOSE_DELAY);
return () => clearTimeout(timer);
}
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed]);
const handleOpenChange = (newOpen: boolean) => {
setIsOpen(newOpen);
};
return (
<ReasoningContext.Provider
value={{ isStreaming, isOpen, setIsOpen, duration }}
>
<Collapsible
className={cn("not-prose mb-4", className)}
onOpenChange={handleOpenChange}
open={isOpen}
{...props}
>
{children}
</Collapsible>
</ReasoningContext.Provider>
);
},
);
export type ReasoningTriggerProps = ComponentProps<
typeof CollapsibleTrigger
> & {
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
};
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
if (isStreaming || duration === 0) {
return <Shimmer duration={1}>Thinking...</Shimmer>;
}
if (duration === undefined) {
return <span>Thought for a few seconds</span>;
}
return <span>Thought for {duration} seconds</span>;
};
export const ReasoningTrigger = memo(
({
className,
children,
getThinkingMessage = defaultGetThinkingMessage,
...props
}: ReasoningTriggerProps) => {
const { isStreaming, isOpen, duration } = useReasoning();
return (
<CollapsibleTrigger
className={cn(
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
className,
)}
{...props}
>
{children ?? (
<>
<BrainIcon className="size-4" />
{getThinkingMessage(isStreaming, duration)}
<ChevronDownIcon
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0",
)}
/>
</>
)}
</CollapsibleTrigger>
);
},
);
export type ReasoningContentProps = ComponentProps<
typeof CollapsibleContent
> & {
children: string;
};
export const ReasoningContent = memo(
({ className, children, ...props }: ReasoningContentProps) => (
<CollapsibleContent
className={cn(
"mt-4 text-sm",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
className,
)}
{...props}
>
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
</CollapsibleContent>
),
);
Reasoning.displayName = "Reasoning";
ReasoningTrigger.displayName = "ReasoningTrigger";
ReasoningContent.displayName = "ReasoningContent";
"use client";
import { useControllableState } from "@radix-ui/react-use-controllable-state";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { BrainIcon, ChevronDownIcon } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { createContext, memo, useContext, useEffect, useState } from "react";
import { Streamdown } from "streamdown";
import { reasoningPlugins } from "@/core/streamdown/plugins";
import { Shimmer } from "./shimmer";
type ReasoningContextValue = {
isStreaming: boolean;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
duration: number | undefined;
};
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
export const useReasoning = () => {
const context = useContext(ReasoningContext);
if (!context) {
throw new Error("Reasoning components must be used within Reasoning");
}
return context;
};
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
isStreaming?: boolean;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
duration?: number;
};
const AUTO_CLOSE_DELAY = 1000;
const MS_IN_S = 1000;
export const Reasoning = memo(
({
className,
isStreaming = false,
open,
defaultOpen = true,
onOpenChange,
duration: durationProp,
children,
...props
}: ReasoningProps) => {
const [isOpen, setIsOpen] = useControllableState({
prop: open,
defaultProp: defaultOpen,
onChange: onOpenChange,
});
const [duration, setDuration] = useControllableState({
prop: durationProp,
defaultProp: undefined,
});
const [hasAutoClosed, setHasAutoClosed] = useState(false);
const [startTime, setStartTime] = useState<number | null>(null);
// Track duration when streaming starts and ends
useEffect(() => {
if (isStreaming) {
if (startTime === null) {
setStartTime(Date.now());
}
} else if (startTime !== null) {
setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S));
setStartTime(null);
}
}, [isStreaming, startTime, setDuration]);
// Auto-open when streaming starts, auto-close when streaming ends (once only)
useEffect(() => {
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
// Add a small delay before closing to allow user to see the content
const timer = setTimeout(() => {
setIsOpen(false);
setHasAutoClosed(true);
}, AUTO_CLOSE_DELAY);
return () => clearTimeout(timer);
}
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed]);
const handleOpenChange = (newOpen: boolean) => {
setIsOpen(newOpen);
};
return (
<ReasoningContext.Provider
value={{ isStreaming, isOpen, setIsOpen, duration }}
>
<Collapsible
className={cn("not-prose mb-4", className)}
onOpenChange={handleOpenChange}
open={isOpen}
{...props}
>
{children}
</Collapsible>
</ReasoningContext.Provider>
);
},
);
export type ReasoningTriggerProps = ComponentProps<
typeof CollapsibleTrigger
> & {
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
};
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
if (isStreaming || duration === 0) {
return <Shimmer duration={1}>Thinking...</Shimmer>;
}
if (duration === undefined) {
return <span>Thought for a few seconds</span>;
}
return <span>Thought for {duration} seconds</span>;
};
export const ReasoningTrigger = memo(
({
className,
children,
getThinkingMessage = defaultGetThinkingMessage,
...props
}: ReasoningTriggerProps) => {
const { isStreaming, isOpen, duration } = useReasoning();
return (
<CollapsibleTrigger
className={cn(
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
className,
)}
{...props}
>
{children ?? (
<>
<BrainIcon className="size-4" />
{getThinkingMessage(isStreaming, duration)}
<ChevronDownIcon
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0",
)}
/>
</>
)}
</CollapsibleTrigger>
);
},
);
export type ReasoningContentProps = ComponentProps<
typeof CollapsibleContent
> & {
children: string;
};
export const ReasoningContent = memo(
({ className, children, ...props }: ReasoningContentProps) => (
<CollapsibleContent
className={cn(
"mt-4 text-sm",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
className,
)}
{...props}
>
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
</CollapsibleContent>
),
);
Reasoning.displayName = "Reasoning";
ReasoningTrigger.displayName = "ReasoningTrigger";
ReasoningContent.displayName = "ReasoningContent";
@@ -1,64 +1,64 @@
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import {
type CSSProperties,
type ElementType,
type JSX,
memo,
useMemo,
} from "react";
export type TextShimmerProps = {
children: string;
as?: ElementType;
className?: string;
duration?: number;
spread?: number;
};
const ShimmerComponent = ({
children,
as: Component = "p",
className,
duration = 2,
spread = 2,
}: TextShimmerProps) => {
const MotionComponent = motion.create(
Component as keyof JSX.IntrinsicElements,
);
const dynamicSpread = useMemo(
() => (children?.length ?? 0) * spread,
[children, spread],
);
return (
<MotionComponent
animate={{ backgroundPosition: "0% center" }}
className={cn(
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
"[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))]",
className,
)}
initial={{ backgroundPosition: "100% center" }}
style={
{
"--spread": `${dynamicSpread}px`,
backgroundImage:
"var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))",
} as CSSProperties
}
transition={{
repeat: Number.POSITIVE_INFINITY,
duration,
ease: "linear",
}}
>
{children}
</MotionComponent>
);
};
export const Shimmer = memo(ShimmerComponent);
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import {
type CSSProperties,
type ElementType,
type JSX,
memo,
useMemo,
} from "react";
export type TextShimmerProps = {
children: string;
as?: ElementType;
className?: string;
duration?: number;
spread?: number;
};
const ShimmerComponent = ({
children,
as: Component = "p",
className,
duration = 2,
spread = 2,
}: TextShimmerProps) => {
const MotionComponent = motion.create(
Component as keyof JSX.IntrinsicElements,
);
const dynamicSpread = useMemo(
() => (children?.length ?? 0) * spread,
[children, spread],
);
return (
<MotionComponent
animate={{ backgroundPosition: "0% center" }}
className={cn(
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
"[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))]",
className,
)}
initial={{ backgroundPosition: "100% center" }}
style={
{
"--spread": `${dynamicSpread}px`,
backgroundImage:
"var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))",
} as CSSProperties
}
transition={{
repeat: Number.POSITIVE_INFINITY,
duration,
ease: "linear",
}}
>
{children}
</MotionComponent>
);
};
export const Shimmer = memo(ShimmerComponent);
@@ -1,77 +1,77 @@
"use client";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { BookIcon, ChevronDownIcon } from "lucide-react";
import type { ComponentProps } from "react";
export type SourcesProps = ComponentProps<"div">;
export const Sources = ({ className, ...props }: SourcesProps) => (
<Collapsible
className={cn("not-prose text-primary mb-4 text-xs", className)}
{...props}
/>
);
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
count: number;
};
export const SourcesTrigger = ({
className,
count,
children,
...props
}: SourcesTriggerProps) => (
<CollapsibleTrigger
className={cn("flex items-center gap-2", className)}
{...props}
>
{children ?? (
<>
<p className="font-medium">Used {count} sources</p>
<ChevronDownIcon className="h-4 w-4" />
</>
)}
</CollapsibleTrigger>
);
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
export const SourcesContent = ({
className,
...props
}: SourcesContentProps) => (
<CollapsibleContent
className={cn(
"mt-3 flex w-fit flex-col gap-2",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
className,
)}
{...props}
/>
);
export type SourceProps = ComponentProps<"a">;
export const Source = ({ href, title, children, ...props }: SourceProps) => (
<a
className="flex items-center gap-2"
href={href}
rel="noopener noreferrer"
target="_blank"
{...props}
>
{children ?? (
<>
<BookIcon className="h-4 w-4" />
<span className="block font-medium">{title}</span>
</>
)}
</a>
);
"use client";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { BookIcon, ChevronDownIcon } from "lucide-react";
import type { ComponentProps } from "react";
export type SourcesProps = ComponentProps<"div">;
export const Sources = ({ className, ...props }: SourcesProps) => (
<Collapsible
className={cn("not-prose text-primary mb-4 text-xs", className)}
{...props}
/>
);
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
count: number;
};
export const SourcesTrigger = ({
className,
count,
children,
...props
}: SourcesTriggerProps) => (
<CollapsibleTrigger
className={cn("flex items-center gap-2", className)}
{...props}
>
{children ?? (
<>
<p className="font-medium">Used {count} sources</p>
<ChevronDownIcon className="h-4 w-4" />
</>
)}
</CollapsibleTrigger>
);
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
export const SourcesContent = ({
className,
...props
}: SourcesContentProps) => (
<CollapsibleContent
className={cn(
"mt-3 flex w-fit flex-col gap-2",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
className,
)}
{...props}
/>
);
export type SourceProps = ComponentProps<"a">;
export const Source = ({ href, title, children, ...props }: SourceProps) => (
<a
className="flex items-center gap-2"
href={href}
rel="noopener noreferrer"
target="_blank"
{...props}
>
{children ?? (
<>
<BookIcon className="h-4 w-4" />
<span className="block font-medium">{title}</span>
</>
)}
</a>
);
@@ -1,80 +1,80 @@
"use client";
import type { LucideIcon } from "lucide-react";
import { Children, type ComponentProps } from "react";
import { Button } from "@/components/ui/button";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
const STAGGER_DELAY_MS = 60;
const STAGGER_DELAY_MS_OFFSET = 250;
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
export const Suggestions = ({
className,
children,
...props
}: SuggestionsProps) => (
<ScrollArea className="overflow-x-auto whitespace-normal" {...props}>
<div
className={cn("flex w-full flex-wrap items-center gap-2", className)}
data-slot="suggestions-list"
>
{Children.map(children, (child, index) =>
child != null ? (
<span
className="animate-fade-in-up max-w-full opacity-0"
style={{
animationDelay: `${STAGGER_DELAY_MS_OFFSET + index * STAGGER_DELAY_MS}ms`,
}}
>
{child}
</span>
) : (
child
),
)}
</div>
<ScrollBar className="hidden" orientation="horizontal" />
</ScrollArea>
);
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
suggestion: React.ReactNode;
icon?: LucideIcon;
onClick?: () => void;
};
export const Suggestion = ({
suggestion,
onClick,
className,
icon: Icon,
variant = "outline",
size = "sm",
children,
...props
}: SuggestionProps) => {
const handleClick = () => {
onClick?.();
};
return (
<Button
className={cn(
"text-muted-foreground dark:bg-background h-auto max-w-full cursor-pointer rounded-full px-4 py-2 text-center text-xs font-normal whitespace-normal",
className,
)}
onClick={handleClick}
size={size}
type="button"
variant={variant}
{...props}
>
{Icon && <Icon className="size-4" />}
{children ?? suggestion}
</Button>
);
};
"use client";
import type { LucideIcon } from "lucide-react";
import { Children, type ComponentProps } from "react";
import { Button } from "@/components/ui/button";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
const STAGGER_DELAY_MS = 60;
const STAGGER_DELAY_MS_OFFSET = 250;
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
export const Suggestions = ({
className,
children,
...props
}: SuggestionsProps) => (
<ScrollArea className="overflow-x-auto whitespace-normal" {...props}>
<div
className={cn("flex w-full flex-wrap items-center gap-2", className)}
data-slot="suggestions-list"
>
{Children.map(children, (child, index) =>
child != null ? (
<span
className="animate-fade-in-up max-w-full opacity-0"
style={{
animationDelay: `${STAGGER_DELAY_MS_OFFSET + index * STAGGER_DELAY_MS}ms`,
}}
>
{child}
</span>
) : (
child
),
)}
</div>
<ScrollBar className="hidden" orientation="horizontal" />
</ScrollArea>
);
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
suggestion: React.ReactNode;
icon?: LucideIcon;
onClick?: () => void;
};
export const Suggestion = ({
suggestion,
onClick,
className,
icon: Icon,
variant = "outline",
size = "sm",
children,
...props
}: SuggestionProps) => {
const handleClick = () => {
onClick?.();
};
return (
<Button
className={cn(
"text-muted-foreground dark:bg-background h-auto max-w-full cursor-pointer rounded-full px-4 py-2 text-center text-xs font-normal whitespace-normal",
className,
)}
onClick={handleClick}
size={size}
type="button"
variant={variant}
{...props}
>
{Icon && <Icon className="size-4" />}
{children ?? suggestion}
</Button>
);
};
@@ -1,87 +1,87 @@
"use client";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { ChevronDownIcon, SearchIcon } from "lucide-react";
import type { ComponentProps } from "react";
export type TaskItemFileProps = ComponentProps<"div">;
export const TaskItemFile = ({
children,
className,
...props
}: TaskItemFileProps) => (
<div
className={cn(
"bg-secondary text-foreground inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs",
className,
)}
{...props}
>
{children}
</div>
);
export type TaskItemProps = ComponentProps<"div">;
export const TaskItem = ({ children, className, ...props }: TaskItemProps) => (
<div className={cn("text-muted-foreground text-sm", className)} {...props}>
{children}
</div>
);
export type TaskProps = ComponentProps<typeof Collapsible>;
export const Task = ({
defaultOpen = true,
className,
...props
}: TaskProps) => (
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
);
export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
title: string;
};
export const TaskTrigger = ({
children,
className,
title,
...props
}: TaskTriggerProps) => (
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
{children ?? (
<div className="text-muted-foreground hover:text-foreground flex w-full cursor-pointer items-center gap-2 text-sm transition-colors">
<SearchIcon className="size-4" />
<p className="text-sm">{title}</p>
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
</div>
)}
</CollapsibleTrigger>
);
export type TaskContentProps = ComponentProps<typeof CollapsibleContent>;
export const TaskContent = ({
children,
className,
...props
}: TaskContentProps) => (
<CollapsibleContent
className={cn(
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
className,
)}
{...props}
>
<div className="border-muted mt-4 space-y-2 border-l-2 pl-4">
{children}
</div>
</CollapsibleContent>
);
"use client";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { ChevronDownIcon, SearchIcon } from "lucide-react";
import type { ComponentProps } from "react";
export type TaskItemFileProps = ComponentProps<"div">;
export const TaskItemFile = ({
children,
className,
...props
}: TaskItemFileProps) => (
<div
className={cn(
"bg-secondary text-foreground inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs",
className,
)}
{...props}
>
{children}
</div>
);
export type TaskItemProps = ComponentProps<"div">;
export const TaskItem = ({ children, className, ...props }: TaskItemProps) => (
<div className={cn("text-muted-foreground text-sm", className)} {...props}>
{children}
</div>
);
export type TaskProps = ComponentProps<typeof Collapsible>;
export const Task = ({
defaultOpen = true,
className,
...props
}: TaskProps) => (
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
);
export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
title: string;
};
export const TaskTrigger = ({
children,
className,
title,
...props
}: TaskTriggerProps) => (
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
{children ?? (
<div className="text-muted-foreground hover:text-foreground flex w-full cursor-pointer items-center gap-2 text-sm transition-colors">
<SearchIcon className="size-4" />
<p className="text-sm">{title}</p>
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
</div>
)}
</CollapsibleTrigger>
);
export type TaskContentProps = ComponentProps<typeof CollapsibleContent>;
export const TaskContent = ({
children,
className,
...props
}: TaskContentProps) => (
<CollapsibleContent
className={cn(
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
className,
)}
{...props}
>
<div className="border-muted mt-4 space-y-2 border-l-2 pl-4">
{children}
</div>
</CollapsibleContent>
);
@@ -1,16 +1,16 @@
import { cn } from "@/lib/utils";
import { NodeToolbar, Position } from "@xyflow/react";
import type { ComponentProps } from "react";
type ToolbarProps = ComponentProps<typeof NodeToolbar>;
export const Toolbar = ({ className, ...props }: ToolbarProps) => (
<NodeToolbar
className={cn(
"bg-background flex items-center gap-1 rounded-sm border p-1.5",
className,
)}
position={Position.Bottom}
{...props}
/>
);
import { cn } from "@/lib/utils";
import { NodeToolbar, Position } from "@xyflow/react";
import type { ComponentProps } from "react";
type ToolbarProps = ComponentProps<typeof NodeToolbar>;
export const Toolbar = ({ className, ...props }: ToolbarProps) => (
<NodeToolbar
className={cn(
"bg-background flex items-center gap-1 rounded-sm border p-1.5",
className,
)}
position={Position.Bottom}
{...props}
/>
);
@@ -1,263 +1,263 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { ChevronDownIcon } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { createContext, useContext, useEffect, useState } from "react";
export type WebPreviewContextValue = {
url: string;
setUrl: (url: string) => void;
consoleOpen: boolean;
setConsoleOpen: (open: boolean) => void;
};
const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);
const useWebPreview = () => {
const context = useContext(WebPreviewContext);
if (!context) {
throw new Error("WebPreview components must be used within a WebPreview");
}
return context;
};
export type WebPreviewProps = ComponentProps<"div"> & {
defaultUrl?: string;
onUrlChange?: (url: string) => void;
};
export const WebPreview = ({
className,
children,
defaultUrl = "",
onUrlChange,
...props
}: WebPreviewProps) => {
const [url, setUrl] = useState(defaultUrl);
const [consoleOpen, setConsoleOpen] = useState(false);
const handleUrlChange = (newUrl: string) => {
setUrl(newUrl);
onUrlChange?.(newUrl);
};
const contextValue: WebPreviewContextValue = {
url,
setUrl: handleUrlChange,
consoleOpen,
setConsoleOpen,
};
return (
<WebPreviewContext.Provider value={contextValue}>
<div
className={cn(
"bg-card flex size-full flex-col rounded-lg border",
className,
)}
{...props}
>
{children}
</div>
</WebPreviewContext.Provider>
);
};
export type WebPreviewNavigationProps = ComponentProps<"div">;
export const WebPreviewNavigation = ({
className,
children,
...props
}: WebPreviewNavigationProps) => (
<div
className={cn("flex items-center gap-1 border-b p-2", className)}
{...props}
>
{children}
</div>
);
export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & {
tooltip?: string;
};
export const WebPreviewNavigationButton = ({
onClick,
disabled,
tooltip,
children,
...props
}: WebPreviewNavigationButtonProps) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="hover:text-foreground h-8 w-8 p-0"
disabled={disabled}
onClick={onClick}
size="sm"
variant="ghost"
{...props}
>
{children}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
export type WebPreviewUrlProps = ComponentProps<typeof Input>;
export const WebPreviewUrl = ({
value,
onChange,
onKeyDown,
...props
}: WebPreviewUrlProps) => {
const { url, setUrl } = useWebPreview();
const [inputValue, setInputValue] = useState(url);
// Sync input value with context URL when it changes externally
useEffect(() => {
setInputValue(url);
}, [url]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
onChange?.(event);
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
const target = event.target as HTMLInputElement;
setUrl(target.value);
}
onKeyDown?.(event);
};
return (
<Input
className="h-8 flex-1 text-sm"
onChange={onChange ?? handleChange}
onKeyDown={handleKeyDown}
placeholder="Enter URL..."
value={value ?? inputValue}
{...props}
/>
);
};
export type WebPreviewBodyProps = ComponentProps<"iframe"> & {
loading?: ReactNode;
};
export const WebPreviewBody = ({
className,
loading,
src,
...props
}: WebPreviewBodyProps) => {
const { url } = useWebPreview();
return (
<div className="flex-1">
<iframe
className={cn("size-full", className)}
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
src={(src ?? url) || undefined}
title="Preview"
{...props}
/>
{loading}
</div>
);
};
export type WebPreviewConsoleProps = ComponentProps<"div"> & {
logs?: Array<{
level: "log" | "warn" | "error";
message: string;
timestamp: Date;
}>;
};
export const WebPreviewConsole = ({
className,
logs = [],
children,
...props
}: WebPreviewConsoleProps) => {
const { consoleOpen, setConsoleOpen } = useWebPreview();
return (
<Collapsible
className={cn("bg-muted/50 border-t font-mono text-sm", className)}
onOpenChange={setConsoleOpen}
open={consoleOpen}
{...props}
>
<CollapsibleTrigger asChild>
<Button
className="hover:bg-muted/50 flex w-full items-center justify-between p-4 text-left font-medium"
variant="ghost"
>
Console
<ChevronDownIcon
className={cn(
"h-4 w-4 transition-transform duration-200",
consoleOpen && "rotate-180",
)}
/>
</Button>
</CollapsibleTrigger>
<CollapsibleContent
className={cn(
"px-4 pb-4",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
)}
>
<div className="max-h-48 space-y-1 overflow-y-auto">
{logs.length === 0 ? (
<p className="text-muted-foreground">No console output</p>
) : (
logs.map((log, index) => (
<div
className={cn(
"text-xs",
log.level === "error" && "text-destructive",
log.level === "warn" && "text-yellow-600",
log.level === "log" && "text-foreground",
)}
key={`${log.timestamp.getTime()}-${index}`}
>
<span className="text-muted-foreground">
{log.timestamp.toLocaleTimeString()}
</span>{" "}
{log.message}
</div>
))
)}
{children}
</div>
</CollapsibleContent>
</Collapsible>
);
};
"use client";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { ChevronDownIcon } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { createContext, useContext, useEffect, useState } from "react";
export type WebPreviewContextValue = {
url: string;
setUrl: (url: string) => void;
consoleOpen: boolean;
setConsoleOpen: (open: boolean) => void;
};
const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);
const useWebPreview = () => {
const context = useContext(WebPreviewContext);
if (!context) {
throw new Error("WebPreview components must be used within a WebPreview");
}
return context;
};
export type WebPreviewProps = ComponentProps<"div"> & {
defaultUrl?: string;
onUrlChange?: (url: string) => void;
};
export const WebPreview = ({
className,
children,
defaultUrl = "",
onUrlChange,
...props
}: WebPreviewProps) => {
const [url, setUrl] = useState(defaultUrl);
const [consoleOpen, setConsoleOpen] = useState(false);
const handleUrlChange = (newUrl: string) => {
setUrl(newUrl);
onUrlChange?.(newUrl);
};
const contextValue: WebPreviewContextValue = {
url,
setUrl: handleUrlChange,
consoleOpen,
setConsoleOpen,
};
return (
<WebPreviewContext.Provider value={contextValue}>
<div
className={cn(
"bg-card flex size-full flex-col rounded-lg border",
className,
)}
{...props}
>
{children}
</div>
</WebPreviewContext.Provider>
);
};
export type WebPreviewNavigationProps = ComponentProps<"div">;
export const WebPreviewNavigation = ({
className,
children,
...props
}: WebPreviewNavigationProps) => (
<div
className={cn("flex items-center gap-1 border-b p-2", className)}
{...props}
>
{children}
</div>
);
export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & {
tooltip?: string;
};
export const WebPreviewNavigationButton = ({
onClick,
disabled,
tooltip,
children,
...props
}: WebPreviewNavigationButtonProps) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="hover:text-foreground h-8 w-8 p-0"
disabled={disabled}
onClick={onClick}
size="sm"
variant="ghost"
{...props}
>
{children}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
export type WebPreviewUrlProps = ComponentProps<typeof Input>;
export const WebPreviewUrl = ({
value,
onChange,
onKeyDown,
...props
}: WebPreviewUrlProps) => {
const { url, setUrl } = useWebPreview();
const [inputValue, setInputValue] = useState(url);
// Sync input value with context URL when it changes externally
useEffect(() => {
setInputValue(url);
}, [url]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
onChange?.(event);
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
const target = event.target as HTMLInputElement;
setUrl(target.value);
}
onKeyDown?.(event);
};
return (
<Input
className="h-8 flex-1 text-sm"
onChange={onChange ?? handleChange}
onKeyDown={handleKeyDown}
placeholder="Enter URL..."
value={value ?? inputValue}
{...props}
/>
);
};
export type WebPreviewBodyProps = ComponentProps<"iframe"> & {
loading?: ReactNode;
};
export const WebPreviewBody = ({
className,
loading,
src,
...props
}: WebPreviewBodyProps) => {
const { url } = useWebPreview();
return (
<div className="flex-1">
<iframe
className={cn("size-full", className)}
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
src={(src ?? url) || undefined}
title="Preview"
{...props}
/>
{loading}
</div>
);
};
export type WebPreviewConsoleProps = ComponentProps<"div"> & {
logs?: Array<{
level: "log" | "warn" | "error";
message: string;
timestamp: Date;
}>;
};
export const WebPreviewConsole = ({
className,
logs = [],
children,
...props
}: WebPreviewConsoleProps) => {
const { consoleOpen, setConsoleOpen } = useWebPreview();
return (
<Collapsible
className={cn("bg-muted/50 border-t font-mono text-sm", className)}
onOpenChange={setConsoleOpen}
open={consoleOpen}
{...props}
>
<CollapsibleTrigger asChild>
<Button
className="hover:bg-muted/50 flex w-full items-center justify-between p-4 text-left font-medium"
variant="ghost"
>
Console
<ChevronDownIcon
className={cn(
"h-4 w-4 transition-transform duration-200",
consoleOpen && "rotate-180",
)}
/>
</Button>
</CollapsibleTrigger>
<CollapsibleContent
className={cn(
"px-4 pb-4",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
)}
>
<div className="max-h-48 space-y-1 overflow-y-auto">
{logs.length === 0 ? (
<p className="text-muted-foreground">No console output</p>
) : (
logs.map((log, index) => (
<div
className={cn(
"text-xs",
log.level === "error" && "text-destructive",
log.level === "warn" && "text-yellow-600",
log.level === "log" && "text-foreground",
)}
key={`${log.timestamp.getTime()}-${index}`}
>
<span className="text-muted-foreground">
{log.timestamp.toLocaleTimeString()}
</span>{" "}
{log.message}
</div>
))
)}
{children}
</div>
</CollapsibleContent>
</Collapsible>
);
};
+19 -19
View File
@@ -1,19 +1,19 @@
import { useMemo } from "react";
export function Footer() {
const year = useMemo(() => new Date().getFullYear(), []);
return (
<footer className="container-md mx-auto mt-32 flex flex-col items-center justify-center">
<hr className="from-border/0 to-border/0 m-0 h-px w-full border-none bg-linear-to-r via-white/20" />
<div className="text-muted-foreground container flex h-20 flex-col items-center justify-center text-sm">
<p className="text-center font-serif text-lg md:text-xl">
&quot;Originated from Open Source, give back to Open Source.&quot;
</p>
</div>
<div className="text-muted-foreground container mb-8 flex flex-col items-center justify-center text-xs">
<p>Licensed under MIT License</p>
<p>&copy; {year} CM业务中心-问答管理</p>
</div>
</footer>
);
}
import { useMemo } from "react";
export function Footer() {
const year = useMemo(() => new Date().getFullYear(), []);
return (
<footer className="container-md mx-auto mt-32 flex flex-col items-center justify-center">
<hr className="from-border/0 to-border/0 m-0 h-px w-full border-none bg-linear-to-r via-white/20" />
<div className="text-muted-foreground container flex h-20 flex-col items-center justify-center text-sm">
<p className="text-center font-serif text-lg md:text-xl">
&quot;Originated from Open Source, give back to Open Source.&quot;
</p>
</div>
<div className="text-muted-foreground container mb-8 flex flex-col items-center justify-center text-xs">
<p>Licensed under MIT License</p>
<p>&copy; {year} CM 助手</p>
</div>
</footer>
);
}
+52 -127
View File
@@ -1,127 +1,52 @@
"use client";
import { StarFilledIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { NumberTicker } from "@/components/ui/number-ticker";
import type { Locale } from "@/core/i18n/locale";
import { useI18n } from "@/core/i18n/hooks";
import { env } from "@/env";
import { cn } from "@/lib/utils";
export type HeaderProps = {
className?: string;
homeURL?: string;
locale?: Locale;
};
export function Header({ className, homeURL }: HeaderProps) {
const isExternalHome = !homeURL;
const { locale: resolvedLocale, t } = useI18n();
const lang = resolvedLocale.substring(0, 2);
return (
<header
className={cn(
"container-md fixed top-0 right-0 left-0 z-20 mx-auto flex h-16 items-center justify-between backdrop-blur-xs",
className,
)}
>
<div className="flex items-center gap-6">
<a
href={homeURL ?? "https://github.com/bytedance/deer-flow"}
target={isExternalHome ? "_blank" : "_self"}
rel={isExternalHome ? "noopener noreferrer" : undefined}
>
<h1 className="font-serif text-xl">CM业务中心-问答管理</h1>
</a>
</div>
<nav className="mr-8 ml-auto flex items-center gap-8 text-sm font-medium">
<Link
href={`/${lang}/docs`}
className="text-secondary-foreground hover:text-foreground transition-colors"
>
{t.home.docs}
</Link>
<Link
href="/blog/posts"
className="text-secondary-foreground hover:text-foreground transition-colors"
>
{t.home.blog}
</Link>
</nav>
<div className="relative">
<div
className="pointer-events-none absolute inset-0 z-0 h-full w-full rounded-full opacity-30 blur-2xl"
style={{
background: "linear-gradient(90deg, #ff80b5 0%, #9089fc 100%)",
filter: "blur(16px)",
}}
/>
<Button
variant="outline"
size="sm"
asChild
className="group relative z-10"
>
<a
href="https://github.com/bytedance/deer-flow"
target="_blank"
rel="noopener noreferrer"
>
<GitHubLogoIcon className="size-4" />
Star on GitHub
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
env.GITHUB_OAUTH_TOKEN && <StarCounter />}
</a>
</Button>
</div>
<hr className="from-border/0 via-border/70 to-border/0 absolute top-16 right-0 left-0 z-10 m-0 h-px w-full border-none bg-linear-to-r" />
</header>
);
}
function StarCounter() {
const [stars, setStars] = useState(10000);
useEffect(() => {
let cancelled = false;
async function loadStars() {
try {
const response = await fetch(
"https://api.github.com/repos/bytedance/deer-flow",
{
headers: env.GITHUB_OAUTH_TOKEN
? {
Authorization: `Bearer ${env.GITHUB_OAUTH_TOKEN}`,
"Content-Type": "application/json",
}
: {},
},
);
if (!response.ok) return;
const data = await response.json();
if (!cancelled && typeof data?.stargazers_count === "number") {
setStars(data.stargazers_count);
}
} catch (error) {
console.error("Error fetching GitHub stars:", error);
}
}
void loadStars();
return () => {
cancelled = true;
};
}, []);
return (
<>
<StarFilledIcon className="size-4 transition-colors duration-300 group-hover:text-yellow-500" />
<NumberTicker className="font-mono tabular-nums" value={stars} />
</>
);
}
"use client";
import Link from "next/link";
import type { Locale } from "@/core/i18n/locale";
import { useI18n } from "@/core/i18n/hooks";
import { cn } from "@/lib/utils";
export type HeaderProps = {
className?: string;
homeURL?: string;
locale?: Locale;
};
export function Header({ className, homeURL }: HeaderProps) {
const isExternalHome = !homeURL;
const { locale: resolvedLocale, t } = useI18n();
const lang = resolvedLocale.substring(0, 2);
return (
<header
className={cn(
"container-md fixed top-0 right-0 left-0 z-20 mx-auto flex h-16 items-center justify-between backdrop-blur-xs",
className,
)}
>
<div className="flex items-center gap-6">
<a
href={homeURL ?? "/"}
target={isExternalHome ? "_blank" : "_self"}
rel={isExternalHome ? "noopener noreferrer" : undefined}
>
<h1 className="font-serif text-xl">CM 助手</h1>
</a>
</div>
<nav className="mr-8 ml-auto flex items-center gap-8 text-sm font-medium">
<Link
href={`/${lang}/docs`}
className="text-secondary-foreground hover:text-foreground transition-colors"
>
{t.home.docs}
</Link>
<Link
href="/blog/posts"
className="text-secondary-foreground hover:text-foreground transition-colors"
>
{t.home.blog}
</Link>
</nav>
<hr className="from-border/0 via-border/70 to-border/0 absolute top-16 right-0 left-0 z-10 m-0 h-px w-full border-none bg-linear-to-r" />
</header>
);
}
+136 -136
View File
@@ -1,136 +1,136 @@
"use client";
import { ChevronRightIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { FlickeringGrid } from "@/components/ui/flickering-grid";
import Galaxy from "@/components/ui/galaxy";
import { WordRotate } from "@/components/ui/word-rotate";
import { env } from "@/env";
import { cn } from "@/lib/utils";
export function Hero({ className }: { className?: string }) {
return (
<div
className={cn(
"flex size-full flex-col items-center justify-center",
className,
)}
>
<div className="absolute inset-0 z-0 bg-black/40">
<Galaxy
mouseRepulsion={false}
starSpeed={0.2}
density={0.6}
glowIntensity={0.35}
twinkleIntensity={0.3}
speed={0.5}
/>
</div>
<FlickeringGrid
className="absolute inset-0 z-0 translate-y-8 mask-[url(/images/deer.svg)] mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
squareSize={4}
gridGap={4}
color={"white"}
maxOpacity={0.3}
flickerChance={0.25}
/>
<div className="container-md relative z-10 mx-auto flex h-screen flex-col items-center justify-center">
<h1 className="flex items-center gap-2 text-4xl font-bold md:text-6xl">
<WordRotate
words={[
"Deep Research",
"Collect Data",
"Analyze Data",
"Generate Webpages",
"Vibe Coding",
"Generate Slides",
"Generate Images",
"Generate Podcasts",
"Generate Videos",
"Generate Songs",
"Organize Emails",
"Do Anything",
"Learn Anything",
]}
/>{" "}
<div>CM业务中心-问答管理</div>
</h1>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
<a
href="https://byteplus.com"
target="_blank"
rel="noopener noreferrer"
>
<div className="mt-4 flex size-fit items-center rounded-lg border border-white px-3 text-center text-shadow-sm">
<span>In partnership with</span>&nbsp;
<BytePlusIcon className="h-4" />
</div>
</a>
)}
<p className="text-muted-foreground mt-8 scale-105 text-center text-2xl text-shadow-sm">
An open-source SuperAgent harness that researches, codes, and creates.
With
<br />
the help of sandboxes, memories, tools, skills and subagents, it
handles
<br />
different levels of tasks that could take minutes to hours.
</p>
<Link href="/workspace">
<Button className="size-lg mt-8 scale-108" size="lg">
<span className="text-md">Get Started with 2.0</span>
<ChevronRightIcon className="size-4" />
</Button>
</Link>
</div>
</div>
);
}
function BytePlusIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={80}
height={24}
viewBox="0 0 120 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M30.8027 3.64648H37.9333C38.8894 3.64648 39.7259 3.81911 40.4297 4.16435C41.1334 4.50959 41.6911 4.98761 42.0762 5.59843C42.4613 6.20924 42.6472 6.88644 42.6472 7.6566C42.6472 8.34708 42.4746 8.99773 42.1426 9.58198C41.7973 10.1795 41.3326 10.631 40.7483 10.9629C41.5583 11.3082 42.1691 11.8128 42.5808 12.49C42.9924 13.1672 43.1916 13.9108 43.1916 14.734C43.1916 15.5175 42.9924 16.208 42.5941 16.8188C42.1957 17.4296 41.638 17.9209 40.921 18.2661C40.2039 18.6246 39.3408 18.7973 38.3449 18.7973H30.8027V3.64648ZM37.8005 9.99361C38.4113 9.99361 38.9026 9.82099 39.2877 9.47575C39.6728 9.13051 39.8587 8.69232 39.8587 8.1479C39.8587 7.5902 39.6728 7.12546 39.301 6.76694C38.9292 6.40842 38.4379 6.22252 37.8138 6.22252H33.5248V9.99361H37.8005ZM38.1458 16.2345C38.7831 16.2345 39.301 16.0619 39.6993 15.7034C40.0977 15.3449 40.2969 14.8934 40.2969 14.349C40.2969 13.9772 40.2039 13.6319 40.018 13.3398C39.8321 13.0477 39.5798 12.8087 39.2479 12.636C38.9292 12.4634 38.5574 12.3705 38.1325 12.3705H33.5381V16.2212H38.1458V16.2345Z"
fill="currentColor"
/>
<path
d="M49.2392 18.2385L44.127 7.3236H46.7295L50.567 15.4899L54.0062 7.3236H57.6445V5.09281L60.2073 4.33594V7.3236H63.0887V9.75357H60.2073V15.304C60.2073 15.7023 60.3135 16.021 60.5259 16.2202C60.7384 16.4327 61.0438 16.5655 61.4422 16.6318C61.8405 16.6982 62.3982 16.7115 63.0887 16.6584V18.9556C61.7741 19.1016 60.7251 19.0618 59.9417 18.8626C59.1582 18.6502 58.574 18.2518 58.2022 17.6676C57.8304 17.0833 57.6445 16.2866 57.6445 15.2907V9.75357H55.5863L49.4782 24.0014H46.7959L49.2392 18.2385Z"
fill="currentColor"
/>
<path
d="M70.2461 19.1147C69.1572 19.1147 68.1746 18.8491 67.2849 18.318C66.3953 17.7869 65.6782 17.0565 65.1604 16.1138C64.6425 15.1843 64.377 14.1485 64.377 13.0066C64.377 11.8912 64.6425 10.882 65.1737 9.97908C65.7048 9.06286 66.4218 8.34582 67.3248 7.82796C68.2277 7.29682 69.2236 7.03125 70.2992 7.03125C71.6536 7.03125 72.7822 7.33666 73.6719 7.93419C74.5616 8.53172 75.2122 9.36827 75.584 10.4438C75.9691 11.5194 76.0753 12.7676 75.916 14.1751L67.0061 14.1884C67.1389 14.7062 67.3779 15.1577 67.7364 15.5295C68.0949 15.9013 68.5066 16.1802 68.958 16.366C69.4095 16.5519 69.8477 16.6316 70.2461 16.6316C70.6975 16.6316 71.1092 16.5918 71.5075 16.4988C71.8926 16.4059 72.2378 16.2731 72.5432 16.0739C72.8486 15.8747 73.1142 15.649 73.3665 15.3967L73.4727 15.2639L75.5043 16.6051C74.9998 17.4416 74.296 18.0657 73.4063 18.4773C72.5167 18.9022 71.4677 19.1147 70.2461 19.1147ZM73.486 11.9576C73.3665 11.4663 73.1673 11.0281 72.8619 10.6563C72.5565 10.2845 72.1847 9.99236 71.7332 9.79318C71.2818 9.594 70.8037 9.48778 70.2992 9.48778C69.8211 9.48778 69.3564 9.594 68.8916 9.79318C68.4269 9.99236 68.0285 10.2845 67.6833 10.6563C67.3381 11.0281 67.1256 11.4663 67.0459 11.9576H73.486Z"
fill="currentColor"
/>
<path
d="M78 3.64648H84.3206C86.0733 3.64648 87.4278 4.0714 88.3838 4.92122C89.3399 5.77105 89.8179 6.93956 89.8179 8.42675C89.8179 9.39608 89.6054 10.2459 89.1805 10.9762C88.7556 11.7065 88.1315 12.2642 87.3215 12.6493C86.5115 13.0344 85.5156 13.2336 84.3471 13.2336H80.7354V18.7973H78V3.64648ZM84.3737 10.7505C84.9314 10.7505 85.4227 10.6575 85.8078 10.4849C86.2061 10.3123 86.5115 10.0467 86.724 9.70149C86.9364 9.35625 87.0427 8.93133 87.0427 8.42675C87.0427 7.6566 86.8169 7.08562 86.3655 6.71382C85.914 6.34202 85.2501 6.15612 84.387 6.15612H80.7487V10.7505H84.3737Z"
fill="currentColor"
/>
<path
d="M92.168 3.36719H94.8104V18.8101H92.168V3.36719Z"
fill="currentColor"
/>
<path
d="M101.982 19.1155C101.158 19.1155 100.415 18.9164 99.764 18.5313C99.1134 18.1462 98.5955 17.6018 98.2237 16.9113C97.8519 16.2208 97.666 15.4507 97.666 14.6008V7.32422H100.282V13.6979C100.282 14.2822 100.375 14.7735 100.574 15.1984C100.773 15.6233 101.052 15.9553 101.424 16.1943C101.796 16.4333 102.247 16.5528 102.765 16.5528C103.256 16.5528 103.708 16.4333 104.106 16.181C104.504 15.942 104.836 15.5835 105.075 15.1187C105.314 14.654 105.434 14.1228 105.434 13.512V7.32422H108.076V18.8101H105.527V17.3628C105.155 17.947 104.651 18.3719 104.04 18.6774C103.442 18.9562 102.752 19.1155 101.982 19.1155Z"
fill="currentColor"
/>
<path
d="M115.287 19.1163C114.424 19.1163 113.535 18.9304 112.645 18.5586C111.755 18.1868 111.012 17.6424 110.414 16.9386L112.1 15.5045C112.525 15.8763 112.95 16.1817 113.375 16.4075C113.813 16.6332 114.358 16.766 115.048 16.8058C115.752 16.8457 116.35 16.766 116.828 16.5535C117.306 16.3411 117.558 16.0357 117.584 15.6107C117.598 15.3186 117.518 15.0929 117.345 14.9335C117.173 14.7742 116.907 14.6281 116.535 14.5219C116.164 14.4157 115.593 14.2962 114.809 14.1634L114.265 14.0572C113.123 13.8713 112.246 13.5127 111.622 12.9949C110.998 12.477 110.693 11.7334 110.693 10.7906C110.693 10.0072 110.905 9.34329 111.317 8.77231C111.742 8.21461 112.286 7.7897 112.99 7.51085C113.681 7.232 114.451 7.08594 115.314 7.08594C116.19 7.08594 117.053 7.24528 117.863 7.56396C118.687 7.88265 119.364 8.40051 119.882 9.10427L118.209 10.5118C117.903 10.1267 117.505 9.83459 117 9.63541C116.496 9.43624 115.951 9.34329 115.354 9.34329C114.969 9.34329 114.597 9.38312 114.252 9.47607C113.906 9.56902 113.614 9.70181 113.388 9.90098C113.163 10.1002 113.043 10.3392 113.043 10.6446C113.043 10.9102 113.123 11.1093 113.269 11.2554C113.415 11.4015 113.694 11.5342 114.079 11.6405C114.464 11.7467 115.128 11.8928 116.044 12.0654H116.084C117.226 12.2778 118.169 12.6762 118.899 13.2339C119.629 13.7916 120.001 14.5617 120.001 15.5045C120.001 16.2348 119.775 16.8855 119.324 17.4299C118.872 17.9743 118.288 18.3992 117.558 18.6914C116.841 18.9702 116.084 19.1163 115.287 19.1163Z"
fill="currentColor"
/>
<path
d="M14.7392 9.80759C14.5931 9.92709 14.3806 9.83414 14.3806 9.63497V8.58596V2.23883C14.3806 2.05293 14.1549 1.9467 14.0221 2.06621L5.17862 9.56857C5.03256 9.68808 4.8201 9.59513 4.8201 9.39595V4.27044C4.8201 4.12438 4.70059 4.00487 4.55453 4.00487H0.26557C0.119507 4.00487 0 4.12438 0 4.27044V20.3242C0 20.5101 0.225735 20.6163 0.35852 20.4968L9.18873 12.9944C9.33479 12.8749 9.54725 12.9679 9.54725 13.1671V20.5765C9.54725 20.7624 9.77299 20.8686 9.90577 20.7491L18.736 13.2467C18.882 13.1272 19.0945 13.2202 19.0945 13.4193V18.5449C19.0945 18.6909 19.214 18.8104 19.3601 18.8104H23.649C23.7951 18.8104 23.9146 18.6909 23.9146 18.5449V2.47784C23.9146 2.29194 23.6889 2.18572 23.5561 2.30522L14.7392 9.80759Z"
fill="currentColor"
/>
</svg>
);
}
"use client";
import { ChevronRightIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { FlickeringGrid } from "@/components/ui/flickering-grid";
import Galaxy from "@/components/ui/galaxy";
import { WordRotate } from "@/components/ui/word-rotate";
import { env } from "@/env";
import { cn } from "@/lib/utils";
export function Hero({ className }: { className?: string }) {
return (
<div
className={cn(
"flex size-full flex-col items-center justify-center",
className,
)}
>
<div className="absolute inset-0 z-0 bg-black/40">
<Galaxy
mouseRepulsion={false}
starSpeed={0.2}
density={0.6}
glowIntensity={0.35}
twinkleIntensity={0.3}
speed={0.5}
/>
</div>
<FlickeringGrid
className="absolute inset-0 z-0 translate-y-8 mask-[url(/images/deer.svg)] mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
squareSize={4}
gridGap={4}
color={"white"}
maxOpacity={0.3}
flickerChance={0.25}
/>
<div className="container-md relative z-10 mx-auto flex h-screen flex-col items-center justify-center">
<h1 className="flex items-center gap-2 text-4xl font-bold md:text-6xl">
<WordRotate
words={[
"Deep Research",
"Collect Data",
"Analyze Data",
"Generate Webpages",
"Vibe Coding",
"Generate Slides",
"Generate Images",
"Generate Podcasts",
"Generate Videos",
"Generate Songs",
"Organize Emails",
"Do Anything",
"Learn Anything",
]}
/>{" "}
<div>CM 助手</div>
</h1>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
<a
href="https://byteplus.com"
target="_blank"
rel="noopener noreferrer"
>
<div className="mt-4 flex size-fit items-center rounded-lg border border-white px-3 text-center text-shadow-sm">
<span>In partnership with</span>&nbsp;
<BytePlusIcon className="h-4" />
</div>
</a>
)}
<p className="text-muted-foreground mt-8 scale-105 text-center text-2xl text-shadow-sm">
An open-source SuperAgent harness that researches, codes, and creates.
With
<br />
the help of sandboxes, memories, tools, skills and subagents, it
handles
<br />
different levels of tasks that could take minutes to hours.
</p>
<Link href="/workspace">
<Button className="size-lg mt-8 scale-108" size="lg">
<span className="text-md">Get Started with 2.0</span>
<ChevronRightIcon className="size-4" />
</Button>
</Link>
</div>
</div>
);
}
function BytePlusIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={80}
height={24}
viewBox="0 0 120 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M30.8027 3.64648H37.9333C38.8894 3.64648 39.7259 3.81911 40.4297 4.16435C41.1334 4.50959 41.6911 4.98761 42.0762 5.59843C42.4613 6.20924 42.6472 6.88644 42.6472 7.6566C42.6472 8.34708 42.4746 8.99773 42.1426 9.58198C41.7973 10.1795 41.3326 10.631 40.7483 10.9629C41.5583 11.3082 42.1691 11.8128 42.5808 12.49C42.9924 13.1672 43.1916 13.9108 43.1916 14.734C43.1916 15.5175 42.9924 16.208 42.5941 16.8188C42.1957 17.4296 41.638 17.9209 40.921 18.2661C40.2039 18.6246 39.3408 18.7973 38.3449 18.7973H30.8027V3.64648ZM37.8005 9.99361C38.4113 9.99361 38.9026 9.82099 39.2877 9.47575C39.6728 9.13051 39.8587 8.69232 39.8587 8.1479C39.8587 7.5902 39.6728 7.12546 39.301 6.76694C38.9292 6.40842 38.4379 6.22252 37.8138 6.22252H33.5248V9.99361H37.8005ZM38.1458 16.2345C38.7831 16.2345 39.301 16.0619 39.6993 15.7034C40.0977 15.3449 40.2969 14.8934 40.2969 14.349C40.2969 13.9772 40.2039 13.6319 40.018 13.3398C39.8321 13.0477 39.5798 12.8087 39.2479 12.636C38.9292 12.4634 38.5574 12.3705 38.1325 12.3705H33.5381V16.2212H38.1458V16.2345Z"
fill="currentColor"
/>
<path
d="M49.2392 18.2385L44.127 7.3236H46.7295L50.567 15.4899L54.0062 7.3236H57.6445V5.09281L60.2073 4.33594V7.3236H63.0887V9.75357H60.2073V15.304C60.2073 15.7023 60.3135 16.021 60.5259 16.2202C60.7384 16.4327 61.0438 16.5655 61.4422 16.6318C61.8405 16.6982 62.3982 16.7115 63.0887 16.6584V18.9556C61.7741 19.1016 60.7251 19.0618 59.9417 18.8626C59.1582 18.6502 58.574 18.2518 58.2022 17.6676C57.8304 17.0833 57.6445 16.2866 57.6445 15.2907V9.75357H55.5863L49.4782 24.0014H46.7959L49.2392 18.2385Z"
fill="currentColor"
/>
<path
d="M70.2461 19.1147C69.1572 19.1147 68.1746 18.8491 67.2849 18.318C66.3953 17.7869 65.6782 17.0565 65.1604 16.1138C64.6425 15.1843 64.377 14.1485 64.377 13.0066C64.377 11.8912 64.6425 10.882 65.1737 9.97908C65.7048 9.06286 66.4218 8.34582 67.3248 7.82796C68.2277 7.29682 69.2236 7.03125 70.2992 7.03125C71.6536 7.03125 72.7822 7.33666 73.6719 7.93419C74.5616 8.53172 75.2122 9.36827 75.584 10.4438C75.9691 11.5194 76.0753 12.7676 75.916 14.1751L67.0061 14.1884C67.1389 14.7062 67.3779 15.1577 67.7364 15.5295C68.0949 15.9013 68.5066 16.1802 68.958 16.366C69.4095 16.5519 69.8477 16.6316 70.2461 16.6316C70.6975 16.6316 71.1092 16.5918 71.5075 16.4988C71.8926 16.4059 72.2378 16.2731 72.5432 16.0739C72.8486 15.8747 73.1142 15.649 73.3665 15.3967L73.4727 15.2639L75.5043 16.6051C74.9998 17.4416 74.296 18.0657 73.4063 18.4773C72.5167 18.9022 71.4677 19.1147 70.2461 19.1147ZM73.486 11.9576C73.3665 11.4663 73.1673 11.0281 72.8619 10.6563C72.5565 10.2845 72.1847 9.99236 71.7332 9.79318C71.2818 9.594 70.8037 9.48778 70.2992 9.48778C69.8211 9.48778 69.3564 9.594 68.8916 9.79318C68.4269 9.99236 68.0285 10.2845 67.6833 10.6563C67.3381 11.0281 67.1256 11.4663 67.0459 11.9576H73.486Z"
fill="currentColor"
/>
<path
d="M78 3.64648H84.3206C86.0733 3.64648 87.4278 4.0714 88.3838 4.92122C89.3399 5.77105 89.8179 6.93956 89.8179 8.42675C89.8179 9.39608 89.6054 10.2459 89.1805 10.9762C88.7556 11.7065 88.1315 12.2642 87.3215 12.6493C86.5115 13.0344 85.5156 13.2336 84.3471 13.2336H80.7354V18.7973H78V3.64648ZM84.3737 10.7505C84.9314 10.7505 85.4227 10.6575 85.8078 10.4849C86.2061 10.3123 86.5115 10.0467 86.724 9.70149C86.9364 9.35625 87.0427 8.93133 87.0427 8.42675C87.0427 7.6566 86.8169 7.08562 86.3655 6.71382C85.914 6.34202 85.2501 6.15612 84.387 6.15612H80.7487V10.7505H84.3737Z"
fill="currentColor"
/>
<path
d="M92.168 3.36719H94.8104V18.8101H92.168V3.36719Z"
fill="currentColor"
/>
<path
d="M101.982 19.1155C101.158 19.1155 100.415 18.9164 99.764 18.5313C99.1134 18.1462 98.5955 17.6018 98.2237 16.9113C97.8519 16.2208 97.666 15.4507 97.666 14.6008V7.32422H100.282V13.6979C100.282 14.2822 100.375 14.7735 100.574 15.1984C100.773 15.6233 101.052 15.9553 101.424 16.1943C101.796 16.4333 102.247 16.5528 102.765 16.5528C103.256 16.5528 103.708 16.4333 104.106 16.181C104.504 15.942 104.836 15.5835 105.075 15.1187C105.314 14.654 105.434 14.1228 105.434 13.512V7.32422H108.076V18.8101H105.527V17.3628C105.155 17.947 104.651 18.3719 104.04 18.6774C103.442 18.9562 102.752 19.1155 101.982 19.1155Z"
fill="currentColor"
/>
<path
d="M115.287 19.1163C114.424 19.1163 113.535 18.9304 112.645 18.5586C111.755 18.1868 111.012 17.6424 110.414 16.9386L112.1 15.5045C112.525 15.8763 112.95 16.1817 113.375 16.4075C113.813 16.6332 114.358 16.766 115.048 16.8058C115.752 16.8457 116.35 16.766 116.828 16.5535C117.306 16.3411 117.558 16.0357 117.584 15.6107C117.598 15.3186 117.518 15.0929 117.345 14.9335C117.173 14.7742 116.907 14.6281 116.535 14.5219C116.164 14.4157 115.593 14.2962 114.809 14.1634L114.265 14.0572C113.123 13.8713 112.246 13.5127 111.622 12.9949C110.998 12.477 110.693 11.7334 110.693 10.7906C110.693 10.0072 110.905 9.34329 111.317 8.77231C111.742 8.21461 112.286 7.7897 112.99 7.51085C113.681 7.232 114.451 7.08594 115.314 7.08594C116.19 7.08594 117.053 7.24528 117.863 7.56396C118.687 7.88265 119.364 8.40051 119.882 9.10427L118.209 10.5118C117.903 10.1267 117.505 9.83459 117 9.63541C116.496 9.43624 115.951 9.34329 115.354 9.34329C114.969 9.34329 114.597 9.38312 114.252 9.47607C113.906 9.56902 113.614 9.70181 113.388 9.90098C113.163 10.1002 113.043 10.3392 113.043 10.6446C113.043 10.9102 113.123 11.1093 113.269 11.2554C113.415 11.4015 113.694 11.5342 114.079 11.6405C114.464 11.7467 115.128 11.8928 116.044 12.0654H116.084C117.226 12.2778 118.169 12.6762 118.899 13.2339C119.629 13.7916 120.001 14.5617 120.001 15.5045C120.001 16.2348 119.775 16.8855 119.324 17.4299C118.872 17.9743 118.288 18.3992 117.558 18.6914C116.841 18.9702 116.084 19.1163 115.287 19.1163Z"
fill="currentColor"
/>
<path
d="M14.7392 9.80759C14.5931 9.92709 14.3806 9.83414 14.3806 9.63497V8.58596V2.23883C14.3806 2.05293 14.1549 1.9467 14.0221 2.06621L5.17862 9.56857C5.03256 9.68808 4.8201 9.59513 4.8201 9.39595V4.27044C4.8201 4.12438 4.70059 4.00487 4.55453 4.00487H0.26557C0.119507 4.00487 0 4.12438 0 4.27044V20.3242C0 20.5101 0.225735 20.6163 0.35852 20.4968L9.18873 12.9944C9.33479 12.8749 9.54725 12.9679 9.54725 13.1671V20.5765C9.54725 20.7624 9.77299 20.8686 9.90577 20.7491L18.736 13.2467C18.882 13.1272 19.0945 13.2202 19.0945 13.4193V18.5449C19.0945 18.6909 19.214 18.8104 19.3601 18.8104H23.649C23.7951 18.8104 23.9146 18.6909 23.9146 18.5449V2.47784C23.9146 2.29194 23.6889 2.18572 23.5561 2.30522L14.7392 9.80759Z"
fill="currentColor"
/>
</svg>
);
}
+163 -163
View File
@@ -1,163 +1,163 @@
import Link from "next/link";
import { getBlogRoute, normalizeTagSlug, type BlogPost } from "@/core/blog";
import { cn } from "@/lib/utils";
type PostListProps = {
description?: string;
posts: BlogPost[];
title: string;
};
type PostMetaProps = {
currentLang?: string;
date?: string | null;
languages?: string[];
pathname?: string;
};
function formatDate(date?: string): string | null {
if (!date) {
return null;
}
const value = new Date(date);
if (Number.isNaN(value.getTime())) {
return date;
}
return new Intl.DateTimeFormat("en-US", {
day: "numeric",
month: "short",
year: "numeric",
}).format(value);
}
export function PostMeta({
currentLang,
date,
languages,
pathname,
}: PostMetaProps) {
const formattedDate = formatDate(date ?? undefined);
const availableLanguages = Array.isArray(languages)
? languages.filter((lang): lang is string => typeof lang === "string")
: [];
if (!formattedDate && availableLanguages.length <= 1) {
return null;
}
return (
<div className="flex flex-wrap items-center gap-8 text-sm">
{formattedDate ? (
<p className="text-muted-foreground">{formattedDate}</p>
) : null}
{pathname && availableLanguages.length > 1 ? (
<div className="flex flex-wrap items-center gap-3">
<span className="text-secondary-foreground text-sm">Language:</span>
{availableLanguages.map((lang) => {
const isActive = lang === currentLang;
return (
<Link
key={lang}
href={`${pathname}?lang=${lang}`}
className={
isActive
? "text-foreground text-sm font-medium"
: "text-muted-foreground hover:text-foreground text-sm transition-colors"
}
>
{lang.toUpperCase()}
</Link>
);
})}
</div>
) : null}
</div>
);
}
export function PostTags({
tags,
className,
}: {
tags?: unknown;
className?: string;
}) {
if (!Array.isArray(tags)) {
return null;
}
const validTags = tags.filter(
(tag): tag is string => typeof tag === "string" && tag.length > 0,
);
if (validTags.length === 0) {
return null;
}
return (
<div className={cn("flex flex-wrap items-center gap-3", className)}>
<span className="text-secondary-foreground text-sm">Tags:</span>
{validTags.map((tag) => (
<Link
key={tag}
href={`/blog/tags/${normalizeTagSlug(tag)}`}
className="border-border text-secondary-foreground hover:text-foreground rounded-xl border px-2 py-1 text-sm transition-colors"
>
{tag}
</Link>
))}
</div>
);
}
export function PostList({ description, posts, title }: PostListProps) {
return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-12 px-6">
<header className="space-y-4">
<h2 className="text-foreground text-4xl font-semibold tracking-tight">
{title}
</h2>
{description ? (
<p className="text-secondary-foreground">{description}</p>
) : null}
</header>
<div className="space-y-12">
{posts.map((post) => {
return (
<article
key={post.slug.join("/")}
className="border-border space-y-5 border-b pb-12 last:border-b-0 last:pb-0"
>
<div className="space-y-3">
<PostMeta
currentLang={post.lang}
date={post.metadata.date}
languages={post.languages}
pathname={getBlogRoute(post.slug)}
/>
<Link
href={getBlogRoute(post.slug)}
className="text-foreground hover:text-primary block text-2xl font-semibold tracking-tight transition-colors"
>
{post.title}
</Link>
</div>
{post.metadata.description ? (
<p className="text-secondary-foreground leading-10">
{post.metadata.description}
</p>
) : null}
<PostTags tags={post.metadata.tags} />
</article>
);
})}
</div>
</div>
);
}
import Link from "next/link";
import { getBlogRoute, normalizeTagSlug, type BlogPost } from "@/core/blog";
import { cn } from "@/lib/utils";
type PostListProps = {
description?: string;
posts: BlogPost[];
title: string;
};
type PostMetaProps = {
currentLang?: string;
date?: string | null;
languages?: string[];
pathname?: string;
};
function formatDate(date?: string): string | null {
if (!date) {
return null;
}
const value = new Date(date);
if (Number.isNaN(value.getTime())) {
return date;
}
return new Intl.DateTimeFormat("en-US", {
day: "numeric",
month: "short",
year: "numeric",
}).format(value);
}
export function PostMeta({
currentLang,
date,
languages,
pathname,
}: PostMetaProps) {
const formattedDate = formatDate(date ?? undefined);
const availableLanguages = Array.isArray(languages)
? languages.filter((lang): lang is string => typeof lang === "string")
: [];
if (!formattedDate && availableLanguages.length <= 1) {
return null;
}
return (
<div className="flex flex-wrap items-center gap-8 text-sm">
{formattedDate ? (
<p className="text-muted-foreground">{formattedDate}</p>
) : null}
{pathname && availableLanguages.length > 1 ? (
<div className="flex flex-wrap items-center gap-3">
<span className="text-secondary-foreground text-sm">Language:</span>
{availableLanguages.map((lang) => {
const isActive = lang === currentLang;
return (
<Link
key={lang}
href={`${pathname}?lang=${lang}`}
className={
isActive
? "text-foreground text-sm font-medium"
: "text-muted-foreground hover:text-foreground text-sm transition-colors"
}
>
{lang.toUpperCase()}
</Link>
);
})}
</div>
) : null}
</div>
);
}
export function PostTags({
tags,
className,
}: {
tags?: unknown;
className?: string;
}) {
if (!Array.isArray(tags)) {
return null;
}
const validTags = tags.filter(
(tag): tag is string => typeof tag === "string" && tag.length > 0,
);
if (validTags.length === 0) {
return null;
}
return (
<div className={cn("flex flex-wrap items-center gap-3", className)}>
<span className="text-secondary-foreground text-sm">Tags:</span>
{validTags.map((tag) => (
<Link
key={tag}
href={`/blog/tags/${normalizeTagSlug(tag)}`}
className="border-border text-secondary-foreground hover:text-foreground rounded-xl border px-2 py-1 text-sm transition-colors"
>
{tag}
</Link>
))}
</div>
);
}
export function PostList({ description, posts, title }: PostListProps) {
return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-12 px-6">
<header className="space-y-4">
<h2 className="text-foreground text-4xl font-semibold tracking-tight">
{title}
</h2>
{description ? (
<p className="text-secondary-foreground">{description}</p>
) : null}
</header>
<div className="space-y-12">
{posts.map((post) => {
return (
<article
key={post.slug.join("/")}
className="border-border space-y-5 border-b pb-12 last:border-b-0 last:pb-0"
>
<div className="space-y-3">
<PostMeta
currentLang={post.lang}
date={post.metadata.date}
languages={post.languages}
pathname={getBlogRoute(post.slug)}
/>
<Link
href={getBlogRoute(post.slug)}
className="text-foreground hover:text-primary block text-2xl font-semibold tracking-tight transition-colors"
>
{post.title}
</Link>
</div>
{post.metadata.description ? (
<p className="text-secondary-foreground leading-10">
{post.metadata.description}
</p>
) : null}
<PostTags tags={post.metadata.tags} />
</article>
);
})}
</div>
</div>
);
}
File diff suppressed because it is too large. Load diff
+29 -29
View File
@@ -1,29 +1,29 @@
import { cn } from "@/lib/utils";
export function Section({
className,
title,
subtitle,
children,
}: {
className?: string;
title: React.ReactNode;
subtitle?: React.ReactNode;
children: React.ReactNode;
}) {
return (
<section className={cn("mx-auto flex flex-col py-16", className)}>
<header className="flex flex-col items-center justify-between">
<div className="mb-4 bg-linear-to-r from-white via-gray-200 to-gray-400 bg-clip-text text-center text-5xl font-bold text-transparent">
{title}
</div>
{subtitle && (
<div className="text-muted-foreground text-center text-xl">
{subtitle}
</div>
)}
</header>
<main className="mt-4">{children}</main>
</section>
);
}
import { cn } from "@/lib/utils";
export function Section({
className,
title,
subtitle,
children,
}: {
className?: string;
title: React.ReactNode;
subtitle?: React.ReactNode;
children: React.ReactNode;
}) {
return (
<section className={cn("mx-auto flex flex-col py-16", className)}>
<header className="flex flex-col items-center justify-between">
<div className="mb-4 bg-linear-to-r from-white via-gray-200 to-gray-400 bg-clip-text text-center text-5xl font-bold text-transparent">
{title}
</div>
{subtitle && (
<div className="text-muted-foreground text-center text-xl">
{subtitle}
</div>
)}
</header>
<main className="mt-4">{children}</main>
</section>
);
}
@@ -1,99 +1,99 @@
import Link from "next/link";
import { Card } from "@/components/ui/card";
import { pathOfThread } from "@/core/threads/utils";
import { cn } from "@/lib/utils";
import { Section } from "../section";
export function CaseStudySection({ className }: { className?: string }) {
const caseStudies = [
{
threadId: "7cfa5f8f-a2f8-47ad-acbd-da7137baf990",
title: "Forecast 2026 Agent Trends and Opportunities",
description:
"Create a webpage with a Deep Research report forecasting the agent technology trends and opportunities in 2026.",
},
{
threadId: "4f3e55ee-f853-43db-bfb3-7d1a411f03cb",
title: 'Generate a Video Based On the Novel "Pride and Prejudice"',
description:
'Search the specific scene from the novel "Pride and Prejudice", then generate a video as well as a reference image based on the scenes.',
},
{
threadId: "21cfea46-34bd-4aa6-9e1f-3009452fbeb9",
title: "Doraemon Explains the MOE Architecture",
description:
"Generate a Doraemon comic strip explaining the MOE architecture to the teenagers who are interested in AI.",
},
{
threadId: "ad76c455-5bf9-4335-8517-fc03834ab828",
title: "An Exploratory Data Analysis of the Titanic Dataset",
description:
"Explore the Titanic dataset and identify the key factors that influenced survival rates with visualizations and insights.",
},
{
threadId: "d3e5adaf-084c-4dd5-9d29-94f1d6bccd98",
title: "Watch Y Combinator's Video then Conduct a Deep Research",
description:
"Watch the given Y Combinator's YouTube video and conduct a deep research on the YC's tips for technical startup founders.",
},
{
threadId: "3823e443-4e2b-4679-b496-a9506eae462b",
title: "Collect and Summarize Dr. Fei Fei Li's Podcasts",
description:
"Collect all the podcast appearances of Dr. Fei Fei Li in the last 6 months, then summarize them into a comprehensive report.",
},
];
return (
<Section
className={className}
title="Case Studies"
subtitle="CM业务中心-问答管理"
>
<div className="container-md mt-8 grid grid-cols-1 gap-4 px-4 md:grid-cols-2 md:px-20 lg:grid-cols-3">
{caseStudies.map((caseStudy) => (
<Link
key={caseStudy.title}
href={pathOfThread(caseStudy.threadId) + "?mock=true"}
target="_blank"
rel="noopener noreferrer"
>
<Card className="group/card relative h-64 overflow-hidden">
<div
className="absolute inset-0 z-0 bg-cover bg-center bg-no-repeat transition-all duration-300 group-hover/card:scale-110 group-hover/card:brightness-90"
style={{
backgroundImage: `url(/images/${caseStudy.threadId}.jpg)`,
}}
></div>
<div
className={cn(
"flex h-full w-full translate-y-[calc(100%-60px)] flex-col items-center",
"transition-all duration-300",
"group-hover/card:translate-y-[calc(100%-128px)]",
)}
>
<div
className="flex w-full flex-col p-4"
style={{
background:
"linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%)",
}}
>
<div className="flex flex-col gap-2">
<h3 className="flex h-14 items-center text-xl font-bold text-shadow-black">
{caseStudy.title}
</h3>
<p className="box-shadow-black overflow-hidden text-sm text-white/85 text-shadow-black">
{caseStudy.description}
</p>
</div>
</div>
</div>
</Card>
</Link>
))}
</div>
</Section>
);
}
import Link from "next/link";
import { Card } from "@/components/ui/card";
import { pathOfThread } from "@/core/threads/utils";
import { cn } from "@/lib/utils";
import { Section } from "../section";
export function CaseStudySection({ className }: { className?: string }) {
const caseStudies = [
{
threadId: "7cfa5f8f-a2f8-47ad-acbd-da7137baf990",
title: "Forecast 2026 Agent Trends and Opportunities",
description:
"Create a webpage with a Deep Research report forecasting the agent technology trends and opportunities in 2026.",
},
{
threadId: "4f3e55ee-f853-43db-bfb3-7d1a411f03cb",
title: 'Generate a Video Based On the Novel "Pride and Prejudice"',
description:
'Search the specific scene from the novel "Pride and Prejudice", then generate a video as well as a reference image based on the scenes.',
},
{
threadId: "21cfea46-34bd-4aa6-9e1f-3009452fbeb9",
title: "Doraemon Explains the MOE Architecture",
description:
"Generate a Doraemon comic strip explaining the MOE architecture to the teenagers who are interested in AI.",
},
{
threadId: "ad76c455-5bf9-4335-8517-fc03834ab828",
title: "An Exploratory Data Analysis of the Titanic Dataset",
description:
"Explore the Titanic dataset and identify the key factors that influenced survival rates with visualizations and insights.",
},
{
threadId: "d3e5adaf-084c-4dd5-9d29-94f1d6bccd98",
title: "Watch Y Combinator's Video then Conduct a Deep Research",
description:
"Watch the given Y Combinator's YouTube video and conduct a deep research on the YC's tips for technical startup founders.",
},
{
threadId: "3823e443-4e2b-4679-b496-a9506eae462b",
title: "Collect and Summarize Dr. Fei Fei Li's Podcasts",
description:
"Collect all the podcast appearances of Dr. Fei Fei Li in the last 6 months, then summarize them into a comprehensive report.",
},
];
return (
<Section
className={className}
title="Case Studies"
subtitle="CM 助手"
>
<div className="container-md mt-8 grid grid-cols-1 gap-4 px-4 md:grid-cols-2 md:px-20 lg:grid-cols-3">
{caseStudies.map((caseStudy) => (
<Link
key={caseStudy.title}
href={pathOfThread(caseStudy.threadId) + "?mock=true"}
target="_blank"
rel="noopener noreferrer"
>
<Card className="group/card relative h-64 overflow-hidden">
<div
className="absolute inset-0 z-0 bg-cover bg-center bg-no-repeat transition-all duration-300 group-hover/card:scale-110 group-hover/card:brightness-90"
style={{
backgroundImage: `url(/images/${caseStudy.threadId}.jpg)`,
}}
></div>
<div
className={cn(
"flex h-full w-full translate-y-[calc(100%-60px)] flex-col items-center",
"transition-all duration-300",
"group-hover/card:translate-y-[calc(100%-128px)]",
)}
>
<div
className="flex w-full flex-col p-4"
style={{
background:
"linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%)",
}}
>
<div className="flex flex-col gap-2">
<h3 className="flex h-14 items-center text-xl font-bold text-shadow-black">
{caseStudy.title}
</h3>
<p className="box-shadow-black overflow-hidden text-sm text-white/85 text-shadow-black">
{caseStudy.description}
</p>
</div>
</div>
</div>
</Card>
</Link>
))}
</div>
</Section>
);
}
@@ -1,35 +1,20 @@
"use client";
import { GitHubLogoIcon } from "@radix-ui/react-icons";
import Link from "next/link";
import { AuroraText } from "@/components/ui/aurora-text";
import { Button } from "@/components/ui/button";
import { Section } from "../section";
export function CommunitySection() {
return (
<Section
title={
<AuroraText colors={["#60A5FA", "#A5FA60", "#A560FA"]}>
Join the Community
</AuroraText>
}
subtitle="Contribute brilliant ideas to shape the future of DeerFlow. Collaborate, innovate, and make impacts."
>
<div className="flex justify-center">
<Button className="text-xl" size="lg" asChild>
<Link
href="https://github.com/bytedance/deer-flow"
target="_blank"
rel="noopener noreferrer"
>
<GitHubLogoIcon />
Contribute Now
</Link>
</Button>
</div>
</Section>
);
}
"use client";
import { AuroraText } from "@/components/ui/aurora-text";
import { Section } from "../section";
export function CommunitySection() {
return (
<Section
title={
<AuroraText colors={["#60A5FA", "#A5FA60", "#A560FA"]}>
Join the Community
</AuroraText>
}
subtitle="Contribute brilliant ideas to shape the future of CM Assistant. Collaborate, innovate, and make impacts."
>
<div />
</Section>
);
}
@@ -1,126 +1,126 @@
"use client";
import {
AnimatedSpan,
Terminal,
TypingAnimation,
} from "@/components/ui/terminal";
import { Section } from "../section";
export function SandboxSection({ className }: { className?: string }) {
return (
<Section
className={className}
title="Agent Runtime Environment"
subtitle={
<p>
We give DeerFlow a &quot;computer&quot;, which can execute commands,
manage files, and run long tasks — all in a secure Docker-based
sandbox
</p>
}
>
<div className="mt-8 flex w-full max-w-6xl flex-col items-center gap-12 lg:flex-row lg:gap-16">
{/* Left: Terminal */}
<div className="w-full flex-1">
<Terminal className="h-[360px] w-full">
{/* Scene 1: Build a Game */}
<TypingAnimation>$ cat requirements.txt</TypingAnimation>
<AnimatedSpan delay={800} className="text-zinc-400">
pygame==2.5.0
</AnimatedSpan>
<TypingAnimation delay={1200}>
$ pip install -r requirements.txt
</TypingAnimation>
<AnimatedSpan delay={2000} className="text-green-500">
✔ Installed pygame
</AnimatedSpan>
<TypingAnimation delay={2400}>
$ write game.py --lines 156
</TypingAnimation>
<AnimatedSpan delay={3200} className="text-blue-500">
✔ Written 156 lines
</AnimatedSpan>
<TypingAnimation delay={3600}>
$ python game.py --test
</TypingAnimation>
<AnimatedSpan delay={4200} className="text-green-500">
✔ All sprites loaded
</AnimatedSpan>
<AnimatedSpan delay={4500} className="text-green-500">
✔ Physics engine OK
</AnimatedSpan>
<AnimatedSpan delay={4800} className="text-green-500">
✔ 60 FPS stable
</AnimatedSpan>
{/* Scene 2: Data Analysis */}
<TypingAnimation delay={5400}>
$ curl -O sales-2024.csv
</TypingAnimation>
<AnimatedSpan delay={6200} className="text-zinc-400">
Downloaded 12.4 MB
</AnimatedSpan>
</Terminal>
</div>
{/* Right: Description */}
<div className="w-full flex-1 space-y-6">
<div className="space-y-4">
<p className="text-sm font-medium tracking-wider text-purple-400 uppercase">
Open-source
</p>
<h2 className="text-4xl font-bold tracking-tight lg:text-5xl">
<a
href="https://github.com/agent-infra/sandbox"
target="_blank"
rel="noopener noreferrer"
>
AIO Sandbox
</a>
</h2>
</div>
<div className="space-y-4 text-lg text-zinc-400">
<p>
We recommend using{" "}
<a
href="https://github.com/agent-infra/sandbox"
className="underline"
target="_blank"
rel="noopener noreferrer"
>
All-in-One Sandbox
</a>{" "}
that combines Browser, Shell, File, MCP and VSCode Server in a
single Docker container.
</p>
</div>
{/* Feature Tags */}
<div className="flex flex-wrap gap-3 pt-4">
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Isolated
</span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Safe
</span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Persistent
</span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Mountable FS
</span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Long-running
</span>
</div>
</div>
</div>
</Section>
);
}
"use client";
import {
AnimatedSpan,
Terminal,
TypingAnimation,
} from "@/components/ui/terminal";
import { Section } from "../section";
export function SandboxSection({ className }: { className?: string }) {
return (
<Section
className={className}
title="Agent Runtime Environment"
subtitle={
<p>
We give CM Assistant a &quot;computer&quot;, which can execute commands,
manage files, and run long tasks — all in a secure Docker-based
sandbox
</p>
}
>
<div className="mt-8 flex w-full max-w-6xl flex-col items-center gap-12 lg:flex-row lg:gap-16">
{/* Left: Terminal */}
<div className="w-full flex-1">
<Terminal className="h-[360px] w-full">
{/* Scene 1: Build a Game */}
<TypingAnimation>$ cat requirements.txt</TypingAnimation>
<AnimatedSpan delay={800} className="text-zinc-400">
pygame==2.5.0
</AnimatedSpan>
<TypingAnimation delay={1200}>
$ pip install -r requirements.txt
</TypingAnimation>
<AnimatedSpan delay={2000} className="text-green-500">
✔ Installed pygame
</AnimatedSpan>
<TypingAnimation delay={2400}>
$ write game.py --lines 156
</TypingAnimation>
<AnimatedSpan delay={3200} className="text-blue-500">
✔ Written 156 lines
</AnimatedSpan>
<TypingAnimation delay={3600}>
$ python game.py --test
</TypingAnimation>
<AnimatedSpan delay={4200} className="text-green-500">
✔ All sprites loaded
</AnimatedSpan>
<AnimatedSpan delay={4500} className="text-green-500">
✔ Physics engine OK
</AnimatedSpan>
<AnimatedSpan delay={4800} className="text-green-500">
✔ 60 FPS stable
</AnimatedSpan>
{/* Scene 2: Data Analysis */}
<TypingAnimation delay={5400}>
$ curl -O sales-2024.csv
</TypingAnimation>
<AnimatedSpan delay={6200} className="text-zinc-400">
Downloaded 12.4 MB
</AnimatedSpan>
</Terminal>
</div>
{/* Right: Description */}
<div className="w-full flex-1 space-y-6">
<div className="space-y-4">
<p className="text-sm font-medium tracking-wider text-purple-400 uppercase">
Open-source
</p>
<h2 className="text-4xl font-bold tracking-tight lg:text-5xl">
<a
href="https://github.com/agent-infra/sandbox"
target="_blank"
rel="noopener noreferrer"
>
AIO Sandbox
</a>
</h2>
</div>
<div className="space-y-4 text-lg text-zinc-400">
<p>
We recommend using{" "}
<a
href="https://github.com/agent-infra/sandbox"
className="underline"
target="_blank"
rel="noopener noreferrer"
>
All-in-One Sandbox
</a>{" "}
that combines Browser, Shell, File, MCP and VSCode Server in a
single Docker container.
</p>
</div>
{/* Feature Tags */}
<div className="flex flex-wrap gap-3 pt-4">
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Isolated
</span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Safe
</span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Persistent
</span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Mountable FS
</span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Long-running
</span>
</div>
</div>
</div>
</Section>
);
}
@@ -1,28 +1,28 @@
"use client";
import { cn } from "@/lib/utils";
import ProgressiveSkillsAnimation from "../progressive-skills-animation";
import { Section } from "../section";
export function SkillsSection({ className }: { className?: string }) {
return (
<Section
className={cn("h-[calc(100vh-64px)] w-full bg-white/2", className)}
title="Agent Skills"
subtitle={
<div>
Agent Skills are loaded progressively — only what&apos;s needed, when
it&apos;s needed.
<br />
Extend DeerFlow with your own skill files, or use our built-in
library.
</div>
}
>
<div className="relative overflow-hidden">
<ProgressiveSkillsAnimation />
</div>
</Section>
);
}
"use client";
import { cn } from "@/lib/utils";
import ProgressiveSkillsAnimation from "../progressive-skills-animation";
import { Section } from "../section";
export function SkillsSection({ className }: { className?: string }) {
return (
<Section
className={cn("h-[calc(100vh-64px)] w-full bg-white/2", className)}
title="Agent Skills"
subtitle={
<div>
Agent Skills are loaded progressively — only what&apos;s needed, when
it&apos;s needed.
<br />
Extend CM Assistant with your own skill files, or use our built-in
library.
</div>
}
>
<div className="relative overflow-hidden">
<ProgressiveSkillsAnimation />
</div>
</Section>
);
}
@@ -1,63 +1,63 @@
"use client";
import MagicBento, { type BentoCardProps } from "@/components/ui/magic-bento";
import { cn } from "@/lib/utils";
import { Section } from "../section";
const COLOR = "#0a0a0a";
const features: BentoCardProps[] = [
{
color: COLOR,
label: "Context Engineering",
title: "Long/Short-term Memory",
description: "Now the agent can better understand you",
},
{
color: COLOR,
label: "Long Task Running",
title: "Planning and Sub-tasking",
description:
"Plans ahead, reasons through complexity, then executes sequentially or in parallel",
},
{
color: COLOR,
label: "Extensible",
title: "Skills and Tools",
description:
"Plug, play, or even swap built-in tools. Build the agent you want.",
},
{
color: COLOR,
label: "Persistent",
title: "Sandbox with File System",
description: "Read, write, run — like a real computer",
},
{
color: COLOR,
label: "Flexible",
title: "Multi-Model Support",
description: "Doubao, DeepSeek, OpenAI, Gemini, etc.",
},
{
color: COLOR,
label: "Free",
title: "Open Source",
description: "MIT License, self-hosted, full control",
},
];
export function WhatsNewSection({ className }: { className?: string }) {
return (
<Section
className={cn("", className)}
title="CM业务中心-问答管理"
subtitle="CM业务中心-问答管理"
>
<div className="flex w-full items-center justify-center">
<MagicBento data={features} />
</div>
</Section>
);
}
"use client";
import MagicBento, { type BentoCardProps } from "@/components/ui/magic-bento";
import { cn } from "@/lib/utils";
import { Section } from "../section";
const COLOR = "#0a0a0a";
const features: BentoCardProps[] = [
{
color: COLOR,
label: "Context Engineering",
title: "Long/Short-term Memory",
description: "Now the agent can better understand you",
},
{
color: COLOR,
label: "Long Task Running",
title: "Planning and Sub-tasking",
description:
"Plans ahead, reasons through complexity, then executes sequentially or in parallel",
},
{
color: COLOR,
label: "Extensible",
title: "Skills and Tools",
description:
"Plug, play, or even swap built-in tools. Build the agent you want.",
},
{
color: COLOR,
label: "Persistent",
title: "Sandbox with File System",
description: "Read, write, run — like a real computer",
},
{
color: COLOR,
label: "Flexible",
title: "Multi-Model Support",
description: "Doubao, DeepSeek, OpenAI, Gemini, etc.",
},
{
color: COLOR,
label: "Free",
title: "Open Source",
description: "MIT License, self-hosted, full control",
},
];
export function WhatsNewSection({ className }: { className?: string }) {
return (
<Section
className={cn("", className)}
title="CM 助手"
subtitle="CM 助手"
>
<div className="flex w-full items-center justify-center">
<MagicBento data={features} />
</div>
</Section>
);
}
@@ -0,0 +1,387 @@
import { useState } from "react";
import type { ElementType } from "react";
import {
BarChart3,
Bot,
Brain,
ChevronLeft,
ChevronRight,
CircleHelp,
Compass,
Database,
FileBox,
FileOutput,
FileText,
Folder,
FolderOpen,
Grid3X3,
HelpCircle,
History,
Layers,
Layout,
Library,
ListTodo,
MessageSquare,
MoreHorizontal,
Network,
PlayCircle,
Plus,
Scale,
Settings,
Target,
TrendingUp,
Users,
Workflow,
} from "lucide-react";
type MenuItem = {
id: string;
label: string;
icon: ElementType;
children?: MenuItem[];
};
type PageSidebarProps = {
collapsed: boolean;
onToggleCollapsed: () => void;
};
const menuItems: MenuItem[] = [
{
id: "qa-center",
label: "QA Center",
icon: HelpCircle,
children: [
{ id: "new-qa", label: "New QA", icon: Plus },
{ id: "general-qa", label: "General QA", icon: CircleHelp },
{ id: "agent-qa", label: "Agent QA", icon: Bot },
{ id: "multi-agent-qa", label: "Multi Agent", icon: Layers },
{ id: "sixstep-chat", label: "Six Step Chat", icon: Workflow },
{ id: "qa-records", label: "QA Records", icon: History },
{ id: "template-editor", label: "Template Config", icon: Settings },
],
},
{
id: "light-app-center",
label: "Light Apps",
icon: Grid3X3,
children: [
{ id: "light-app-square", label: "App Square", icon: Layout },
{ id: "light-app-records", label: "Run Records", icon: History },
],
},
{
id: "task-management",
label: "Task Management",
icon: Target,
children: [
{ id: "dashboard", label: "Dashboard", icon: BarChart3 },
{
id: "xk-task",
label: "XK Tasks",
icon: Folder,
children: [
{ id: "xk-task-jc", label: "Task JC", icon: ListTodo },
{ id: "xk-task-dispatch", label: "Task Dispatch", icon: ListTodo },
],
},
{
id: "plan-task",
label: "Planning Tasks",
icon: Folder,
children: [
{ id: "plan-task-fx", label: "Task Analysis", icon: ListTodo },
{ id: "plan-task-action", label: "Action Strategy", icon: ListTodo },
],
},
{
id: "mnty-task",
label: "MNTY Tasks",
icon: Folder,
children: [
{ id: "mnty-task-mechanism", label: "Mechanism Study", icon: ListTodo },
{ id: "mnty-task-plan", label: "Plan Task", icon: ListTodo },
{ id: "mnty-task-zsty", label: "ZSTY Task", icon: ListTodo },
],
},
{
id: "effect-task",
label: "Effect PG Tasks",
icon: Folder,
children: [
{ id: "effect-task-pg", label: "Task PG", icon: ListTodo },
{ id: "effect-action-pg", label: "Action PG", icon: ListTodo },
{ id: "effect-info-pg", label: "Info PG", icon: ListTodo },
{ id: "effect-account-pg", label: "Account PG", icon: ListTodo },
],
},
{ id: "task-create", label: "Create Task", icon: Plus },
{ id: "task-single", label: "Single Agent Task", icon: Bot },
{ id: "task-scene", label: "Scene Task", icon: Workflow },
{ id: "task-consult-create", label: "Multi Agent Consult", icon: MessageSquare },
{ id: "final-plan", label: "Final Output", icon: FileOutput },
],
},
{ id: "scene-list", label: "Scene Management", icon: Layers },
{
id: "agent-list",
label: "Agent Management",
icon: Bot,
children: [
{ id: "agent-overview", label: "Agent List", icon: Users },
{ id: "agent-trial", label: "Single Agent Trial", icon: PlayCircle },
],
},
{
id: "result-management",
label: "Result Management",
icon: BarChart3,
children: [
{
id: "result-planning",
label: "Planning Results",
icon: Target,
children: [
{ id: "result-planning-all", label: "All", icon: BarChart3 },
{ id: "result-planning-goal", label: "Goal Analysis", icon: Target },
{ id: "result-planning-brand", label: "Brand Strategy", icon: Target },
{ id: "result-planning-content", label: "Content Planning", icon: Target },
{ id: "result-planning-channel", label: "Channel Planning", icon: Target },
{ id: "result-planning-narrative", label: "Narrative Strategy", icon: Target },
],
},
{
id: "result-mnty",
label: "MNTY Results",
icon: Brain,
children: [
{ id: "result-mnty-all", label: "All", icon: BarChart3 },
{ id: "result-mnty-mechanism", label: "Mechanism Research", icon: Brain },
{ id: "result-mnty-plan", label: "Plan Design", icon: Brain },
{ id: "result-mnty-zsty", label: "ZSTY Analysis", icon: Brain },
],
},
{
id: "result-effect-pg",
label: "Effect PG Results",
icon: TrendingUp,
children: [
{ id: "result-effect-pg-all", label: "All", icon: BarChart3 },
{ id: "result-effect-pg-behavior", label: "Behavior PG", icon: TrendingUp },
{ id: "result-effect-pg-evaluation", label: "Effect Review", icon: TrendingUp },
{ id: "result-effect-pg-info", label: "Info PG", icon: TrendingUp },
{ id: "result-effect-pg-account", label: "Account PG", icon: TrendingUp },
],
},
{
id: "result-xk",
label: "XK Results",
icon: Compass,
children: [
{ id: "result-xk-all", label: "All", icon: BarChart3 },
{ id: "result-xk-jc", label: "Task JC", icon: Compass },
{ id: "result-xk-dispatch", label: "Task Dispatch", icon: Compass },
],
},
{
id: "result-dq",
label: "DQ Results",
icon: Database,
children: [
{ id: "result-dq-all", label: "All", icon: BarChart3 },
{ id: "result-dq-audience", label: "Audience Analysis", icon: Database },
{ id: "result-dq-kb", label: "Knowledge Research", icon: Database },
{ id: "result-dq-trend", label: "Trend Research", icon: Database },
],
},
{
id: "result-other",
label: "Other Results",
icon: MoreHorizontal,
children: [
{ id: "result-other-all", label: "All", icon: BarChart3 },
{ id: "result-other-general", label: "General Analysis", icon: MoreHorizontal },
{ id: "result-other-temp", label: "Temp Tasks", icon: MoreHorizontal },
{ id: "result-other-other", label: "Other", icon: MoreHorizontal },
],
},
],
},
{
id: "resource-management",
label: "Resource Management",
icon: Database,
children: [
{ id: "resource-zz", label: "ZZ Resources", icon: FileBox },
{ id: "resource-network-actor", label: "Network Actor", icon: Network },
{ id: "resource-knowledge-base", label: "Knowledge Base", icon: Library },
{ id: "resource-evaluation-index", label: "Evaluation Index", icon: Scale },
],
},
];
const defaultExpandedIds = new Set([
"qa-center",
"task-management",
"xk-task",
"result-management",
"result-planning",
]);
export function PageSidebar({
collapsed,
onToggleCollapsed,
}: PageSidebarProps) {
const [expandedIds, setExpandedIds] = useState<Set<string>>(defaultExpandedIds);
const iconButtonClass =
"rounded-lg p-2 text-gray-500 transition-all duration-300 ease-out hover:scale-105 hover:bg-red-50 hover:text-red-600";
const panelClass = "ml-4 space-y-1 border-l border-red-100 pl-4";
const labelClass = (compact: boolean, small = false) =>
`overflow-hidden whitespace-nowrap transition-all duration-300 ease-out ${
compact ? "max-w-0 opacity-0" : "max-w-[220px] opacity-100"
} ${small ? "text-xs" : "text-sm"} flex-1 font-medium`;
const toggleExpanded = (id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const renderLeaf = (item: MenuItem, depth = 0) => {
const Icon = item.icon;
return (
<button
key={item.id}
type="button"
title={collapsed && depth === 0 ? item.label : undefined}
className={`flex w-full items-center rounded-lg py-2 text-left text-gray-600 transition-all duration-300 ease-out hover:-translate-y-0.5 hover:bg-red-50 hover:text-red-600 ${
collapsed && depth === 0 ? "justify-center px-2" : "gap-2 px-3"
}`}
>
<Icon className={`${depth === 0 ? "h-5 w-5" : "h-4 w-4"} flex-shrink-0 transition-transform duration-300 ease-out`} />
{(!collapsed || depth > 0) && (
<span className={labelClass(false, depth > 0)}>
{item.label}
</span>
)}
</button>
);
};
const renderBranch = (item: MenuItem, depth = 0) => {
const expanded = expandedIds.has(item.id);
const isTopLevel = depth === 0;
const Icon = depth > 0 && item.icon === Folder && expanded ? FolderOpen : item.icon;
return (
<div key={item.id} className="space-y-1">
<button
type="button"
onClick={() => {
if (collapsed && isTopLevel) {
onToggleCollapsed();
return;
}
toggleExpanded(item.id);
}}
title={collapsed && isTopLevel ? item.label : undefined}
className={`flex w-full items-center rounded-lg py-2 text-left text-gray-600 transition-all duration-300 ease-out hover:-translate-y-0.5 hover:bg-red-50 hover:text-red-600 ${
collapsed && isTopLevel ? "justify-center px-2" : "gap-2 px-3"
}`}
>
<Icon
className={`${isTopLevel ? "h-5 w-5" : "h-4 w-4"} flex-shrink-0 transition-transform duration-300 ease-out ${
expanded && depth > 0 ? "scale-105" : ""
}`}
/>
{(!collapsed || !isTopLevel) && (
<span className={labelClass(false, depth > 0)}>
{item.label}
</span>
)}
{(!collapsed || !isTopLevel) && (
<ChevronRight
className={`h-4 w-4 transition-transform duration-300 ease-out ${expanded ? "rotate-90" : ""}`}
/>
)}
</button>
{(!collapsed || !isTopLevel) && item.children && (
<div
className={`${panelClass} overflow-hidden transition-all duration-300 ease-out ${
expanded ? "max-h-[1200px] opacity-100" : "max-h-0 opacity-0"
}`}
>
{item.children.map((child) =>
child.children ? renderBranch(child, depth + 1) : renderLeaf(child, depth + 1),
)}
</div>
)}
</div>
);
};
return (
<aside
className={`fixed left-0 top-0 z-40 h-screen overflow-y-auto border-r border-red-100 bg-white shadow-sm transition-all duration-300 ${
collapsed ? "w-16" : "w-64"
}`}
>
<div
className={`flex h-16 items-center border-b border-red-100 ${
collapsed ? "justify-center px-2" : "justify-between px-6"
}`}
>
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-red-500 to-red-400 transition-transform duration-300 ease-out">
<FileText className="h-4 w-4 text-white" />
</div>
{!collapsed && (
<div className="overflow-hidden whitespace-nowrap animate-in fade-in-0 slide-in-from-left-2 duration-300">
<h1 className="text-sm font-semibold text-gray-900">DeerFlow</h1>
<p className="text-xs text-red-400/80">Page Navigation</p>
</div>
)}
</div>
{!collapsed && (
<button
type="button"
onClick={onToggleCollapsed}
className={iconButtonClass}
title="Collapse sidebar"
>
<ChevronLeft className="h-4 w-4" />
</button>
)}
</div>
<nav className={`${collapsed ? "p-2" : "p-4"} space-y-2 transition-all duration-300 ease-out`}>
{collapsed && (
<button
type="button"
onClick={onToggleCollapsed}
className={`flex w-full items-center justify-center ${iconButtonClass}`}
title="Expand sidebar"
>
<ChevronRight className="h-5 w-5" />
</button>
)}
{menuItems.map((item) =>
item.children ? renderBranch(item) : renderLeaf(item),
)}
</nav>
<div className="pointer-events-none absolute right-0 top-0 h-32 w-32 rounded-full bg-red-500/5 blur-3xl" />
<div className="pointer-events-none absolute bottom-20 right-0 h-24 w-24 rounded-full bg-red-400/5 blur-3xl" />
</aside>
);
}
@@ -1,20 +1,20 @@
"use client";
import {
QueryClient,
QueryClientProvider as TanStackQueryClientProvider,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
export function QueryClientProvider({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<TanStackQueryClientProvider client={queryClient}>
{children}
</TanStackQueryClientProvider>
);
}
"use client";
import {
QueryClient,
QueryClientProvider as TanStackQueryClientProvider,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
export function QueryClientProvider({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<TanStackQueryClientProvider client={queryClient}>
{children}
</TanStackQueryClientProvider>
);
}
+19 -19
View File
@@ -1,19 +1,19 @@
"use client";
import { usePathname } from "next/navigation";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
const pathname = usePathname();
return (
<NextThemesProvider
{...props}
forcedTheme={pathname === "/landing" ? "dark" : undefined}
>
{children}
</NextThemesProvider>
);
}
"use client";
import { usePathname } from "next/navigation";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
const pathname = usePathname();
return (
<NextThemesProvider
{...props}
forcedTheme={pathname === "/landing" ? "dark" : undefined}
>
{children}
</NextThemesProvider>
);
}
+66 -66
View File
@@ -1,66 +1,66 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className,
)}
{...props}
/>
);
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className,
)}
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription };
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className,
)}
{...props}
/>
);
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className,
)}
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription };
+43 -43
View File
@@ -1,43 +1,43 @@
"use client";
import React, { memo } from "react";
interface AuroraTextProps {
children: React.ReactNode;
className?: string;
colors?: string[];
speed?: number;
}
export const AuroraText = memo(
({
children,
className = "",
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
speed = 1,
}: AuroraTextProps) => {
const gradientStyle = {
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
colors[0]
})`,
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
animationDuration: `${10 / speed}s`,
};
return (
<span className={`relative inline-block ${className}`}>
<span className="sr-only">{children}</span>
<span
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
style={gradientStyle}
aria-hidden="true"
>
{children}
</span>
</span>
);
},
);
AuroraText.displayName = "AuroraText";
"use client";
import React, { memo } from "react";
interface AuroraTextProps {
children: React.ReactNode;
className?: string;
colors?: string[];
speed?: number;
}
export const AuroraText = memo(
({
children,
className = "",
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
speed = 1,
}: AuroraTextProps) => {
const gradientStyle = {
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
colors[0]
})`,
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
animationDuration: `${10 / speed}s`,
};
return (
<span className={`relative inline-block ${className}`}>
<span className="sr-only">{children}</span>
<span
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
style={gradientStyle}
aria-hidden="true"
>
{children}
</span>
</span>
);
},
);
AuroraText.displayName = "AuroraText";
+53 -53
View File
@@ -1,53 +1,53 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className,
)}
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback };
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className,
)}
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback };
+46 -46
View File
@@ -1,46 +1,46 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };
+109 -109
View File
@@ -1,109 +1,109 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className,
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
);
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
);
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className,
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
);
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
);
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};
+83 -83
View File
@@ -1,83 +1,83 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
{
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
},
},
defaultVariants: {
orientation: "horizontal",
},
},
);
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
);
}
function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "div";
return (
<Comp
className={cn(
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
className,
)}
{...props}
/>
);
}
export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
};
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
{
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
},
},
defaultVariants: {
orientation: "horizontal",
},
},
);
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
);
}
function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "div";
return (
<Comp
className={cn(
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
className,
)}
{...props}
/>
);
}
export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
};
+63 -63
View File
@@ -1,63 +1,63 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"cursor-pointer bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"cursor-pointer bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"cursor-pointer border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"cursor-pointer bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"cursor-pointer hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "cursor-pointer text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
{...(variant !== undefined && { "data-variant": variant })}
{...(size !== undefined && { "data-size": size })}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"cursor-pointer bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"cursor-pointer bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"cursor-pointer border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"cursor-pointer bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"cursor-pointer hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "cursor-pointer text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
{...(variant !== undefined && { "data-variant": variant })}
{...(size !== undefined && { "data-size": size })}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
+92 -92
View File
@@ -1,92 +1,92 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};
+241 -241
View File
@@ -1,241 +1,241 @@
"use client";
import * as React from "react";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return;
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) return;
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) return;
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel();
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel();
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
);
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
);
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};
"use client";
import * as React from "react";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return;
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) return;
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) return;
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel();
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel();
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
);
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
);
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};
+37 -37
View File
@@ -1,37 +1,37 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
import { cn } from "@/lib/utils";
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({
className,
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
className={cn("cursor-pointer", className)}
{...props}
/>
);
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
import { cn } from "@/lib/utils";
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({
className,
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
className={cn("cursor-pointer", className)}
{...props}
/>
);
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
+181 -181
View File
@@ -1,181 +1,181 @@
"use client";
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from "@/components/ui/dialog";
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className,
)}
{...props}
/>
);
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string;
description?: string;
className?: string;
showCloseButton?: boolean;
}) {
return (
<Dialog {...props}>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<DialogTitle className="sr-only">{title}</DialogTitle>
<DialogDescription className="sr-only">{description}</DialogDescription>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
);
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className,
)}
{...props}
/>
);
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className,
)}
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
);
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
"use client";
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from "@/components/ui/dialog";
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className,
)}
{...props}
/>
);
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string;
description?: string;
className?: string;
showCloseButton?: boolean;
}) {
return (
<Dialog {...props}>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<DialogTitle className="sr-only">{title}</DialogTitle>
<DialogDescription className="sr-only">{description}</DialogDescription>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
);
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className,
)}
{...props}
/>
);
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className,
)}
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
);
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
@@ -1,49 +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>
);
}
"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>
);
}
+143 -143
View File
@@ -1,143 +1,143 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};
+257 -257
View File
@@ -1,257 +1,257 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};
+104 -104
View File
@@ -1,104 +1,104 @@
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className,
)}
{...props}
/>
);
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center",
className,
)}
{...props}
/>
);
}
const emptyMediaVariants = cva(
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
},
},
defaultVariants: {
variant: "default",
},
},
);
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
);
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
{...props}
/>
);
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
className,
)}
{...props}
/>
);
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className,
)}
{...props}
/>
);
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
};
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className,
)}
{...props}
/>
);
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center",
className,
)}
{...props}
/>
);
}
const emptyMediaVariants = cva(
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
},
},
defaultVariants: {
variant: "default",
},
},
);
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
);
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
{...props}
/>
);
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
className,
)}
{...props}
/>
);
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className,
)}
{...props}
/>
);
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
};
+202 -202
View File
@@ -1,202 +1,202 @@
"use client";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> {
squareSize?: number;
gridGap?: number;
flickerChance?: number;
color?: string;
width?: number;
height?: number;
className?: string;
maxOpacity?: number;
}
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
squareSize = 4,
gridGap = 6,
flickerChance = 0.3,
color = "rgb(0, 0, 0)",
width,
height,
className,
maxOpacity = 0.3,
...props
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isInView, setIsInView] = useState(false);
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
const memoizedColor = useMemo(() => {
const toRGBA = (color: string) => {
if (typeof window === "undefined") {
return `rgba(0, 0, 0,`;
}
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 1;
const ctx = canvas.getContext("2d");
if (!ctx) return "rgba(255, 0, 0,";
ctx.fillStyle = color;
ctx.fillRect(0, 0, 1, 1);
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
return `rgba(${r}, ${g}, ${b},`;
};
return toRGBA(color);
}, [color]);
const setupCanvas = useCallback(
(canvas: HTMLCanvasElement, width: number, height: number) => {
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const cols = Math.floor(width / (squareSize + gridGap));
const rows = Math.floor(height / (squareSize + gridGap));
const squares = new Float32Array(cols * rows);
for (let i = 0; i < squares.length; i++) {
squares[i] = Math.random() * maxOpacity;
}
return { cols, rows, squares, dpr };
},
[squareSize, gridGap, maxOpacity],
);
const updateSquares = useCallback(
(squares: Float32Array, deltaTime: number) => {
for (let i = 0; i < squares.length; i++) {
if (Math.random() < flickerChance * deltaTime) {
squares[i] = Math.random() * maxOpacity;
}
}
},
[flickerChance, maxOpacity],
);
const drawGrid = useCallback(
(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
cols: number,
rows: number,
squares: Float32Array,
dpr: number,
) => {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "transparent";
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const opacity = squares[i * rows + j];
ctx.fillStyle = `${memoizedColor}${opacity})`;
ctx.fillRect(
i * (squareSize + gridGap) * dpr,
j * (squareSize + gridGap) * dpr,
squareSize * dpr,
squareSize * dpr,
);
}
}
},
[memoizedColor, squareSize, gridGap],
);
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animationFrameId: number;
let gridParams: ReturnType<typeof setupCanvas>;
const updateCanvasSize = () => {
const newWidth = width || container.clientWidth;
const newHeight = height || container.clientHeight;
setCanvasSize({ width: newWidth, height: newHeight });
gridParams = setupCanvas(canvas, newWidth, newHeight);
};
updateCanvasSize();
let lastTime = 0;
const animate = (time: number) => {
if (!isInView) return;
const deltaTime = (time - lastTime) / 1000;
lastTime = time;
updateSquares(gridParams.squares, deltaTime);
drawGrid(
ctx,
canvas.width,
canvas.height,
gridParams.cols,
gridParams.rows,
gridParams.squares,
gridParams.dpr,
);
animationFrameId = requestAnimationFrame(animate);
};
const resizeObserver = new ResizeObserver(() => {
updateCanvasSize();
});
resizeObserver.observe(container);
const intersectionObserver = new IntersectionObserver(
([entry]) => {
if (entry) {
setIsInView(entry.isIntersecting);
}
},
{ threshold: 0 },
);
intersectionObserver.observe(canvas);
if (isInView) {
animationFrameId = requestAnimationFrame(animate);
}
return () => {
cancelAnimationFrame(animationFrameId);
resizeObserver.disconnect();
intersectionObserver.disconnect();
};
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
return (
<div
ref={containerRef}
className={cn(`h-full w-full ${className}`)}
{...props}
>
<canvas
ref={canvasRef}
className="pointer-events-none"
style={{
width: canvasSize.width,
height: canvasSize.height,
}}
/>
</div>
);
};
"use client";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> {
squareSize?: number;
gridGap?: number;
flickerChance?: number;
color?: string;
width?: number;
height?: number;
className?: string;
maxOpacity?: number;
}
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
squareSize = 4,
gridGap = 6,
flickerChance = 0.3,
color = "rgb(0, 0, 0)",
width,
height,
className,
maxOpacity = 0.3,
...props
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isInView, setIsInView] = useState(false);
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
const memoizedColor = useMemo(() => {
const toRGBA = (color: string) => {
if (typeof window === "undefined") {
return `rgba(0, 0, 0,`;
}
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 1;
const ctx = canvas.getContext("2d");
if (!ctx) return "rgba(255, 0, 0,";
ctx.fillStyle = color;
ctx.fillRect(0, 0, 1, 1);
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
return `rgba(${r}, ${g}, ${b},`;
};
return toRGBA(color);
}, [color]);
const setupCanvas = useCallback(
(canvas: HTMLCanvasElement, width: number, height: number) => {
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const cols = Math.floor(width / (squareSize + gridGap));
const rows = Math.floor(height / (squareSize + gridGap));
const squares = new Float32Array(cols * rows);
for (let i = 0; i < squares.length; i++) {
squares[i] = Math.random() * maxOpacity;
}
return { cols, rows, squares, dpr };
},
[squareSize, gridGap, maxOpacity],
);
const updateSquares = useCallback(
(squares: Float32Array, deltaTime: number) => {
for (let i = 0; i < squares.length; i++) {
if (Math.random() < flickerChance * deltaTime) {
squares[i] = Math.random() * maxOpacity;
}
}
},
[flickerChance, maxOpacity],
);
const drawGrid = useCallback(
(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
cols: number,
rows: number,
squares: Float32Array,
dpr: number,
) => {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "transparent";
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const opacity = squares[i * rows + j];
ctx.fillStyle = `${memoizedColor}${opacity})`;
ctx.fillRect(
i * (squareSize + gridGap) * dpr,
j * (squareSize + gridGap) * dpr,
squareSize * dpr,
squareSize * dpr,
);
}
}
},
[memoizedColor, squareSize, gridGap],
);
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animationFrameId: number;
let gridParams: ReturnType<typeof setupCanvas>;
const updateCanvasSize = () => {
const newWidth = width || container.clientWidth;
const newHeight = height || container.clientHeight;
setCanvasSize({ width: newWidth, height: newHeight });
gridParams = setupCanvas(canvas, newWidth, newHeight);
};
updateCanvasSize();
let lastTime = 0;
const animate = (time: number) => {
if (!isInView) return;
const deltaTime = (time - lastTime) / 1000;
lastTime = time;
updateSquares(gridParams.squares, deltaTime);
drawGrid(
ctx,
canvas.width,
canvas.height,
gridParams.cols,
gridParams.rows,
gridParams.squares,
gridParams.dpr,
);
animationFrameId = requestAnimationFrame(animate);
};
const resizeObserver = new ResizeObserver(() => {
updateCanvasSize();
});
resizeObserver.observe(container);
const intersectionObserver = new IntersectionObserver(
([entry]) => {
if (entry) {
setIsInView(entry.isIntersecting);
}
},
{ threshold: 0 },
);
intersectionObserver.observe(canvas);
if (isInView) {
animationFrameId = requestAnimationFrame(animate);
}
return () => {
cancelAnimationFrame(animationFrameId);
resizeObserver.disconnect();
intersectionObserver.disconnect();
};
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
return (
<div
ref={containerRef}
className={cn(`h-full w-full ${className}`)}
{...props}
>
<canvas
ref={canvasRef}
className="pointer-events-none"
style={{
width: canvasSize.width,
height: canvasSize.height,
}}
/>
</div>
);
};
+5 -5
View File
@@ -1,5 +1,5 @@
.galaxy-container {
width: 100%;
height: 100%;
position: relative;
}
.galaxy-container {
width: 100%;
height: 100%;
position: relative;
}
+361 -361
View File
@@ -1,361 +1,361 @@
import { Renderer, Program, Mesh, Color, Triangle } from "ogl";
import { useEffect, useRef } from "react";
import "./galaxy.css";
const vertexShader = `
attribute vec2 uv;
attribute vec2 position;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0, 1);
}
`;
const fragmentShader = `
precision highp float;
uniform float uTime;
uniform vec3 uResolution;
uniform vec2 uFocal;
uniform vec2 uRotation;
uniform float uStarSpeed;
uniform float uDensity;
uniform float uHueShift;
uniform float uSpeed;
uniform vec2 uMouse;
uniform float uGlowIntensity;
uniform float uSaturation;
uniform bool uMouseRepulsion;
uniform float uTwinkleIntensity;
uniform float uRotationSpeed;
uniform float uRepulsionStrength;
uniform float uMouseActiveFactor;
uniform float uAutoCenterRepulsion;
uniform bool uTransparent;
varying vec2 vUv;
#define NUM_LAYER 4.0
#define STAR_COLOR_CUTOFF 0.2
#define MAT45 mat2(0.7071, -0.7071, 0.7071, 0.7071)
#define PERIOD 3.0
float Hash21(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float tri(float x) {
return abs(fract(x) * 2.0 - 1.0);
}
float tris(float x) {
float t = fract(x);
return 1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0));
}
float trisn(float x) {
float t = fract(x);
return 2.0 * (1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0))) - 1.0;
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
float Star(vec2 uv, float flare) {
float d = length(uv);
float m = (0.05 * uGlowIntensity) / d;
float rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));
m += rays * flare * uGlowIntensity;
uv *= MAT45;
rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));
m += rays * 0.3 * flare * uGlowIntensity;
m *= smoothstep(1.0, 0.2, d);
return m;
}
vec3 StarLayer(vec2 uv) {
vec3 col = vec3(0.0);
vec2 gv = fract(uv) - 0.5;
vec2 id = floor(uv);
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
vec2 offset = vec2(float(x), float(y));
vec2 si = id + vec2(float(x), float(y));
float seed = Hash21(si);
float size = fract(seed * 345.32);
float glossLocal = tri(uStarSpeed / (PERIOD * seed + 1.0));
float flareSize = smoothstep(0.9, 1.0, size) * glossLocal;
float red = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 1.0)) + STAR_COLOR_CUTOFF;
float blu = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 3.0)) + STAR_COLOR_CUTOFF;
float grn = min(red, blu) * seed;
vec3 base = vec3(red, grn, blu);
float hue = atan(base.g - base.r, base.b - base.r) / (2.0 * 3.14159) + 0.5;
hue = fract(hue + uHueShift / 360.0);
float sat = length(base - vec3(dot(base, vec3(0.299, 0.587, 0.114)))) * uSaturation;
float val = max(max(base.r, base.g), base.b);
base = hsv2rgb(vec3(hue, sat, val));
vec2 pad = vec2(tris(seed * 34.0 + uTime * uSpeed / 10.0), tris(seed * 38.0 + uTime * uSpeed / 30.0)) - 0.5;
float star = Star(gv - offset - pad, flareSize);
vec3 color = base;
float twinkle = trisn(uTime * uSpeed + seed * 6.2831) * 0.5 + 1.0;
twinkle = mix(1.0, twinkle, uTwinkleIntensity);
star *= twinkle;
col += star * size * color;
}
}
return col;
}
void main() {
vec2 focalPx = uFocal * uResolution.xy;
vec2 uv = (vUv * uResolution.xy - focalPx) / uResolution.y;
vec2 mouseNorm = uMouse - vec2(0.5);
if (uAutoCenterRepulsion > 0.0) {
vec2 centerUV = vec2(0.0, 0.0);
float centerDist = length(uv - centerUV);
vec2 repulsion = normalize(uv - centerUV) * (uAutoCenterRepulsion / (centerDist + 0.1));
uv += repulsion * 0.05;
} else if (uMouseRepulsion) {
vec2 mousePosUV = (uMouse * uResolution.xy - focalPx) / uResolution.y;
float mouseDist = length(uv - mousePosUV);
vec2 repulsion = normalize(uv - mousePosUV) * (uRepulsionStrength / (mouseDist + 0.1));
uv += repulsion * 0.05 * uMouseActiveFactor;
} else {
vec2 mouseOffset = mouseNorm * 0.1 * uMouseActiveFactor;
uv += mouseOffset;
}
float autoRotAngle = uTime * uRotationSpeed;
mat2 autoRot = mat2(cos(autoRotAngle), -sin(autoRotAngle), sin(autoRotAngle), cos(autoRotAngle));
uv = autoRot * uv;
uv = mat2(uRotation.x, -uRotation.y, uRotation.y, uRotation.x) * uv;
vec3 col = vec3(0.0);
for (float i = 0.0; i < 1.0; i += 1.0 / NUM_LAYER) {
float depth = fract(i + uStarSpeed * uSpeed);
float scale = mix(20.0 * uDensity, 0.5 * uDensity, depth);
float fade = depth * smoothstep(1.0, 0.9, depth);
col += StarLayer(uv * scale + i * 453.32) * fade;
}
if (uTransparent) {
float alpha = length(col);
alpha = smoothstep(0.0, 0.3, alpha);
alpha = min(alpha, 1.0);
gl_FragColor = vec4(col, alpha);
} else {
gl_FragColor = vec4(col, 1.0);
}
}
`;
export default function Galaxy({
focal = [0.5, 0.5],
rotation = [1.0, 0.0],
starSpeed = 0.5,
density = 1,
hueShift = 140,
disableAnimation = false,
speed = 1.0,
mouseInteraction = true,
glowIntensity = 0.3,
saturation = 0.0,
mouseRepulsion = true,
repulsionStrength = 2,
twinkleIntensity = 0.3,
rotationSpeed = 0.1,
autoCenterRepulsion = 0,
transparent = true,
...rest
}) {
const ctnDom = useRef(null);
const targetMousePos = useRef({ x: 0.5, y: 0.5 });
const smoothMousePos = useRef({ x: 0.5, y: 0.5 });
const targetMouseActive = useRef(0.0);
const smoothMouseActive = useRef(0.0);
useEffect(() => {
if (!ctnDom.current) return;
const ctn = ctnDom.current;
let renderer;
try {
renderer = new Renderer({
alpha: transparent,
premultipliedAlpha: false,
});
} catch (error) {
console.warn(
"Galaxy: WebGL is not available. The galaxy background will not be rendered.",
error,
);
return;
}
const gl = renderer.gl;
if (!gl) {
console.warn(
"Galaxy: WebGL context is null. The galaxy background will not be rendered.",
);
return;
}
if (transparent) {
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0, 0, 0, 0);
} else {
gl.clearColor(0, 0, 0, 1);
}
/** @type {Program | undefined} */
let program;
function resize() {
const scale = 1;
renderer.setSize(ctn.offsetWidth * scale, ctn.offsetHeight * scale);
if (program) {
program.uniforms.uResolution.value = new Color(
gl.canvas.width,
gl.canvas.height,
gl.canvas.width / gl.canvas.height,
);
}
}
window.addEventListener("resize", resize, false);
resize();
const geometry = new Triangle(gl);
program = new Program(gl, {
vertex: vertexShader,
fragment: fragmentShader,
uniforms: {
uTime: { value: 0 },
uResolution: {
value: new Color(
gl.canvas.width,
gl.canvas.height,
gl.canvas.width / gl.canvas.height,
),
},
uFocal: { value: new Float32Array(focal) },
uRotation: { value: new Float32Array(rotation) },
uStarSpeed: { value: starSpeed },
uDensity: { value: density },
uHueShift: { value: hueShift },
uSpeed: { value: speed },
uMouse: {
value: new Float32Array([
smoothMousePos.current.x,
smoothMousePos.current.y,
]),
},
uGlowIntensity: { value: glowIntensity },
uSaturation: { value: saturation },
uMouseRepulsion: { value: mouseRepulsion },
uTwinkleIntensity: { value: twinkleIntensity },
uRotationSpeed: { value: rotationSpeed },
uRepulsionStrength: { value: repulsionStrength },
uMouseActiveFactor: { value: 0.0 },
uAutoCenterRepulsion: { value: autoCenterRepulsion },
uTransparent: { value: transparent },
},
});
const mesh = new Mesh(gl, { geometry, program });
let animateId;
function update(t) {
animateId = requestAnimationFrame(update);
if (!disableAnimation) {
program.uniforms.uTime.value = t * 0.001;
program.uniforms.uStarSpeed.value = (t * 0.001 * starSpeed) / 10.0;
}
const lerpFactor = 0.05;
smoothMousePos.current.x +=
(targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor;
smoothMousePos.current.y +=
(targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor;
smoothMouseActive.current +=
(targetMouseActive.current - smoothMouseActive.current) * lerpFactor;
program.uniforms.uMouse.value[0] = smoothMousePos.current.x;
program.uniforms.uMouse.value[1] = smoothMousePos.current.y;
program.uniforms.uMouseActiveFactor.value = smoothMouseActive.current;
renderer.render({ scene: mesh });
}
animateId = requestAnimationFrame(update);
ctn.appendChild(gl.canvas);
function handleMouseMove(e) {
const rect = ctn.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = 1.0 - (e.clientY - rect.top) / rect.height;
targetMousePos.current = { x, y };
targetMouseActive.current = 1.0;
}
function handleMouseLeave() {
targetMouseActive.current = 0.0;
}
if (mouseInteraction) {
ctn.addEventListener("mousemove", handleMouseMove);
ctn.addEventListener("mouseleave", handleMouseLeave);
}
return () => {
cancelAnimationFrame(animateId);
window.removeEventListener("resize", resize);
if (mouseInteraction) {
ctn.removeEventListener("mousemove", handleMouseMove);
ctn.removeEventListener("mouseleave", handleMouseLeave);
}
ctn.removeChild(gl.canvas);
gl.getExtension("WEBGL_lose_context")?.loseContext();
};
}, [
focal,
rotation,
starSpeed,
density,
hueShift,
disableAnimation,
speed,
mouseInteraction,
glowIntensity,
saturation,
mouseRepulsion,
twinkleIntensity,
rotationSpeed,
repulsionStrength,
autoCenterRepulsion,
transparent,
]);
return <div ref={ctnDom} className="galaxy-container" {...rest} />;
}
import { Renderer, Program, Mesh, Color, Triangle } from "ogl";
import { useEffect, useRef } from "react";
import "./galaxy.css";
const vertexShader = `
attribute vec2 uv;
attribute vec2 position;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0, 1);
}
`;
const fragmentShader = `
precision highp float;
uniform float uTime;
uniform vec3 uResolution;
uniform vec2 uFocal;
uniform vec2 uRotation;
uniform float uStarSpeed;
uniform float uDensity;
uniform float uHueShift;
uniform float uSpeed;
uniform vec2 uMouse;
uniform float uGlowIntensity;
uniform float uSaturation;
uniform bool uMouseRepulsion;
uniform float uTwinkleIntensity;
uniform float uRotationSpeed;
uniform float uRepulsionStrength;
uniform float uMouseActiveFactor;
uniform float uAutoCenterRepulsion;
uniform bool uTransparent;
varying vec2 vUv;
#define NUM_LAYER 4.0
#define STAR_COLOR_CUTOFF 0.2
#define MAT45 mat2(0.7071, -0.7071, 0.7071, 0.7071)
#define PERIOD 3.0
float Hash21(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float tri(float x) {
return abs(fract(x) * 2.0 - 1.0);
}
float tris(float x) {
float t = fract(x);
return 1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0));
}
float trisn(float x) {
float t = fract(x);
return 2.0 * (1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0))) - 1.0;
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
float Star(vec2 uv, float flare) {
float d = length(uv);
float m = (0.05 * uGlowIntensity) / d;
float rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));
m += rays * flare * uGlowIntensity;
uv *= MAT45;
rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));
m += rays * 0.3 * flare * uGlowIntensity;
m *= smoothstep(1.0, 0.2, d);
return m;
}
vec3 StarLayer(vec2 uv) {
vec3 col = vec3(0.0);
vec2 gv = fract(uv) - 0.5;
vec2 id = floor(uv);
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
vec2 offset = vec2(float(x), float(y));
vec2 si = id + vec2(float(x), float(y));
float seed = Hash21(si);
float size = fract(seed * 345.32);
float glossLocal = tri(uStarSpeed / (PERIOD * seed + 1.0));
float flareSize = smoothstep(0.9, 1.0, size) * glossLocal;
float red = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 1.0)) + STAR_COLOR_CUTOFF;
float blu = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 3.0)) + STAR_COLOR_CUTOFF;
float grn = min(red, blu) * seed;
vec3 base = vec3(red, grn, blu);
float hue = atan(base.g - base.r, base.b - base.r) / (2.0 * 3.14159) + 0.5;
hue = fract(hue + uHueShift / 360.0);
float sat = length(base - vec3(dot(base, vec3(0.299, 0.587, 0.114)))) * uSaturation;
float val = max(max(base.r, base.g), base.b);
base = hsv2rgb(vec3(hue, sat, val));
vec2 pad = vec2(tris(seed * 34.0 + uTime * uSpeed / 10.0), tris(seed * 38.0 + uTime * uSpeed / 30.0)) - 0.5;
float star = Star(gv - offset - pad, flareSize);
vec3 color = base;
float twinkle = trisn(uTime * uSpeed + seed * 6.2831) * 0.5 + 1.0;
twinkle = mix(1.0, twinkle, uTwinkleIntensity);
star *= twinkle;
col += star * size * color;
}
}
return col;
}
void main() {
vec2 focalPx = uFocal * uResolution.xy;
vec2 uv = (vUv * uResolution.xy - focalPx) / uResolution.y;
vec2 mouseNorm = uMouse - vec2(0.5);
if (uAutoCenterRepulsion > 0.0) {
vec2 centerUV = vec2(0.0, 0.0);
float centerDist = length(uv - centerUV);
vec2 repulsion = normalize(uv - centerUV) * (uAutoCenterRepulsion / (centerDist + 0.1));
uv += repulsion * 0.05;
} else if (uMouseRepulsion) {
vec2 mousePosUV = (uMouse * uResolution.xy - focalPx) / uResolution.y;
float mouseDist = length(uv - mousePosUV);
vec2 repulsion = normalize(uv - mousePosUV) * (uRepulsionStrength / (mouseDist + 0.1));
uv += repulsion * 0.05 * uMouseActiveFactor;
} else {
vec2 mouseOffset = mouseNorm * 0.1 * uMouseActiveFactor;
uv += mouseOffset;
}
float autoRotAngle = uTime * uRotationSpeed;
mat2 autoRot = mat2(cos(autoRotAngle), -sin(autoRotAngle), sin(autoRotAngle), cos(autoRotAngle));
uv = autoRot * uv;
uv = mat2(uRotation.x, -uRotation.y, uRotation.y, uRotation.x) * uv;
vec3 col = vec3(0.0);
for (float i = 0.0; i < 1.0; i += 1.0 / NUM_LAYER) {
float depth = fract(i + uStarSpeed * uSpeed);
float scale = mix(20.0 * uDensity, 0.5 * uDensity, depth);
float fade = depth * smoothstep(1.0, 0.9, depth);
col += StarLayer(uv * scale + i * 453.32) * fade;
}
if (uTransparent) {
float alpha = length(col);
alpha = smoothstep(0.0, 0.3, alpha);
alpha = min(alpha, 1.0);
gl_FragColor = vec4(col, alpha);
} else {
gl_FragColor = vec4(col, 1.0);
}
}
`;
export default function Galaxy({
focal = [0.5, 0.5],
rotation = [1.0, 0.0],
starSpeed = 0.5,
density = 1,
hueShift = 140,
disableAnimation = false,
speed = 1.0,
mouseInteraction = true,
glowIntensity = 0.3,
saturation = 0.0,
mouseRepulsion = true,
repulsionStrength = 2,
twinkleIntensity = 0.3,
rotationSpeed = 0.1,
autoCenterRepulsion = 0,
transparent = true,
...rest
}) {
const ctnDom = useRef(null);
const targetMousePos = useRef({ x: 0.5, y: 0.5 });
const smoothMousePos = useRef({ x: 0.5, y: 0.5 });
const targetMouseActive = useRef(0.0);
const smoothMouseActive = useRef(0.0);
useEffect(() => {
if (!ctnDom.current) return;
const ctn = ctnDom.current;
let renderer;
try {
renderer = new Renderer({
alpha: transparent,
premultipliedAlpha: false,
});
} catch (error) {
console.warn(
"Galaxy: WebGL is not available. The galaxy background will not be rendered.",
error,
);
return;
}
const gl = renderer.gl;
if (!gl) {
console.warn(
"Galaxy: WebGL context is null. The galaxy background will not be rendered.",
);
return;
}
if (transparent) {
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0, 0, 0, 0);
} else {
gl.clearColor(0, 0, 0, 1);
}
/** @type {Program | undefined} */
let program;
function resize() {
const scale = 1;
renderer.setSize(ctn.offsetWidth * scale, ctn.offsetHeight * scale);
if (program) {
program.uniforms.uResolution.value = new Color(
gl.canvas.width,
gl.canvas.height,
gl.canvas.width / gl.canvas.height,
);
}
}
window.addEventListener("resize", resize, false);
resize();
const geometry = new Triangle(gl);
program = new Program(gl, {
vertex: vertexShader,
fragment: fragmentShader,
uniforms: {
uTime: { value: 0 },
uResolution: {
value: new Color(
gl.canvas.width,
gl.canvas.height,
gl.canvas.width / gl.canvas.height,
),
},
uFocal: { value: new Float32Array(focal) },
uRotation: { value: new Float32Array(rotation) },
uStarSpeed: { value: starSpeed },
uDensity: { value: density },
uHueShift: { value: hueShift },
uSpeed: { value: speed },
uMouse: {
value: new Float32Array([
smoothMousePos.current.x,
smoothMousePos.current.y,
]),
},
uGlowIntensity: { value: glowIntensity },
uSaturation: { value: saturation },
uMouseRepulsion: { value: mouseRepulsion },
uTwinkleIntensity: { value: twinkleIntensity },
uRotationSpeed: { value: rotationSpeed },
uRepulsionStrength: { value: repulsionStrength },
uMouseActiveFactor: { value: 0.0 },
uAutoCenterRepulsion: { value: autoCenterRepulsion },
uTransparent: { value: transparent },
},
});
const mesh = new Mesh(gl, { geometry, program });
let animateId;
function update(t) {
animateId = requestAnimationFrame(update);
if (!disableAnimation) {
program.uniforms.uTime.value = t * 0.001;
program.uniforms.uStarSpeed.value = (t * 0.001 * starSpeed) / 10.0;
}
const lerpFactor = 0.05;
smoothMousePos.current.x +=
(targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor;
smoothMousePos.current.y +=
(targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor;
smoothMouseActive.current +=
(targetMouseActive.current - smoothMouseActive.current) * lerpFactor;
program.uniforms.uMouse.value[0] = smoothMousePos.current.x;
program.uniforms.uMouse.value[1] = smoothMousePos.current.y;
program.uniforms.uMouseActiveFactor.value = smoothMouseActive.current;
renderer.render({ scene: mesh });
}
animateId = requestAnimationFrame(update);
ctn.appendChild(gl.canvas);
function handleMouseMove(e) {
const rect = ctn.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = 1.0 - (e.clientY - rect.top) / rect.height;
targetMousePos.current = { x, y };
targetMouseActive.current = 1.0;
}
function handleMouseLeave() {
targetMouseActive.current = 0.0;
}
if (mouseInteraction) {
ctn.addEventListener("mousemove", handleMouseMove);
ctn.addEventListener("mouseleave", handleMouseLeave);
}
return () => {
cancelAnimationFrame(animateId);
window.removeEventListener("resize", resize);
if (mouseInteraction) {
ctn.removeEventListener("mousemove", handleMouseMove);
ctn.removeEventListener("mouseleave", handleMouseLeave);
}
ctn.removeChild(gl.canvas);
gl.getExtension("WEBGL_lose_context")?.loseContext();
};
}, [
focal,
rotation,
starSpeed,
density,
hueShift,
disableAnimation,
speed,
mouseInteraction,
glowIntensity,
saturation,
mouseRepulsion,
twinkleIntensity,
rotationSpeed,
repulsionStrength,
autoCenterRepulsion,
transparent,
]);
return <div ref={ctnDom} className="galaxy-container" {...rest} />;
}
+44 -44
View File
@@ -1,44 +1,44 @@
"use client";
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils";
function HoverCard({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
);
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className,
)}
{...props}
/>
</HoverCardPrimitive.Portal>
);
}
export { HoverCard, HoverCardTrigger, HoverCardContent };
"use client";
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils";
function HoverCard({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
);
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className,
)}
{...props}
/>
</HoverCardPrimitive.Portal>
);
}
export { HoverCard, HoverCardTrigger, HoverCardContent };
+170 -170
View File
@@ -1,170 +1,170 @@
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group border-input/50 dark:bg-background/80 relative flex w-full items-center rounded-md border bg-white/80 shadow-xs transition-[color,box-shadow] outline-none",
"h-9 min-w-0 has-[>textarea]:h-auto",
// Variants based on alignment.
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
// Focus state.
"has-[[data-slot=input-group-control]:focus-visible]:border-input has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
// Error state.
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
className,
)}
{...props}
/>
);
}
const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
{
variants: {
align: {
"inline-start":
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
"inline-end":
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
"block-start":
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
"block-end":
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
},
},
defaultVariants: {
align: "inline-start",
},
},
);
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...props}
/>
);
}
const inputGroupButtonVariants = cva(
"text-sm shadow-none flex gap-2 items-center",
{
variants: {
size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
},
);
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
className,
)}
{...props}
/>
);
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
className,
)}
{...props}
/>
);
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
};
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group border-input/50 dark:bg-background/80 relative flex w-full items-center rounded-md border bg-white/80 shadow-xs transition-[color,box-shadow] outline-none",
"h-9 min-w-0 has-[>textarea]:h-auto",
// Variants based on alignment.
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
// Focus state.
"has-[[data-slot=input-group-control]:focus-visible]:border-input has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
// Error state.
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
className,
)}
{...props}
/>
);
}
const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
{
variants: {
align: {
"inline-start":
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
"inline-end":
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
"block-start":
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
"block-end":
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
},
},
defaultVariants: {
align: "inline-start",
},
},
);
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...props}
/>
);
}
const inputGroupButtonVariants = cva(
"text-sm shadow-none flex gap-2 items-center",
{
variants: {
size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
},
);
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
className,
)}
{...props}
/>
);
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
className,
)}
{...props}
/>
);
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
};
+21 -21
View File
@@ -1,21 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };
import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };
+193 -193
View File
@@ -1,193 +1,193 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="list"
data-slot="item-group"
className={cn("group/item-group flex flex-col", className)}
{...props}
/>
);
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn("my-0", className)}
{...props}
/>
);
}
const itemVariants = cva(
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "p-4 gap-4 ",
sm: "py-3 px-4 gap-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Item({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
);
}
const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
{
variants: {
variant: {
default: "bg-transparent",
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image:
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
},
);
function ItemMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
);
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-content"
className={cn(
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
className,
)}
{...props}
/>
);
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-title"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
className,
)}
{...props}
/>
);
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="item-description"
className={cn(
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className,
)}
{...props}
/>
);
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-actions"
className={cn("flex items-center gap-2", className)}
{...props}
/>
);
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-header"
className={cn(
"flex basis-full items-center justify-between gap-2",
className,
)}
{...props}
/>
);
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-footer"
className={cn(
"flex basis-full items-center justify-between gap-2",
className,
)}
{...props}
/>
);
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
};
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="list"
data-slot="item-group"
className={cn("group/item-group flex flex-col", className)}
{...props}
/>
);
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn("my-0", className)}
{...props}
/>
);
}
const itemVariants = cva(
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "p-4 gap-4 ",
sm: "py-3 px-4 gap-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Item({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
);
}
const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
{
variants: {
variant: {
default: "bg-transparent",
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image:
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
},
);
function ItemMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
);
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-content"
className={cn(
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
className,
)}
{...props}
/>
);
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-title"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
className,
)}
{...props}
/>
);
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="item-description"
className={cn(
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className,
)}
{...props}
/>
);
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-actions"
className={cn("flex items-center gap-2", className)}
{...props}
/>
);
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-header"
className={cn(
"flex basis-full items-center justify-between gap-2",
className,
)}
{...props}
/>
);
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-footer"
className={cn(
"flex basis-full items-center justify-between gap-2",
className,
)}
{...props}
/>
);
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
};
+217 -217
View File
@@ -1,217 +1,217 @@
:root {
--hue: 27;
--sat: 69%;
--white: hsl(0, 0%, 100%);
--purple-primary: rgba(132, 0, 255, 1);
--purple-glow: rgba(132, 0, 255, 0.2);
--purple-border: rgba(132, 0, 255, 0.8);
--border-color: #392e4e;
--background-dark: #060010;
color-scheme: light dark;
}
.card-grid {
display: grid;
gap: 0.5em;
padding: 0.75em;
max-width: 54em;
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);
}
.magic-bento-card {
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
aspect-ratio: 4/3;
min-height: 200px;
width: 100%;
max-width: 100%;
padding: 1.25em;
border-radius: 20px;
border: 1px solid var(--border-color);
background: var(--background-dark);
font-weight: 300;
overflow: hidden;
transition: all 0.3s ease;
--glow-x: 50%;
--glow-y: 50%;
--glow-intensity: 0;
--glow-radius: 200px;
}
.magic-bento-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.magic-bento-card__header,
.magic-bento-card__content {
display: flex;
position: relative;
color: var(--white);
}
.magic-bento-card__header {
gap: 0.75em;
justify-content: space-between;
}
.magic-bento-card__content {
flex-direction: column;
}
.magic-bento-card__label {
font-size: 16px;
}
.magic-bento-card__title,
.magic-bento-card__description {
--clamp-title: 1;
--clamp-desc: 2;
}
.magic-bento-card__title {
font-weight: 400;
font-size: 16px;
margin: 0 0 0.25em;
}
.magic-bento-card__description {
font-size: 12px;
line-height: 1.2;
opacity: 0.9;
}
.magic-bento-card--text-autohide .magic-bento-card__title,
.magic-bento-card--text-autohide .magic-bento-card__description {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.magic-bento-card--text-autohide .magic-bento-card__title {
-webkit-line-clamp: var(--clamp-title);
line-clamp: var(--clamp-title);
}
.magic-bento-card--text-autohide .magic-bento-card__description {
-webkit-line-clamp: var(--clamp-desc);
line-clamp: var(--clamp-desc);
}
@media (max-width: 599px) {
.card-grid {
grid-template-columns: 1fr;
width: 90%;
margin: 0 auto;
padding: 0.5em;
}
.magic-bento-card {
width: 100%;
min-height: 180px;
}
}
@media (min-width: 600px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.card-grid {
grid-template-columns: repeat(4, 1fr);
}
.magic-bento-card:nth-child(3) {
grid-column: span 2;
grid-row: span 2;
}
.magic-bento-card:nth-child(4) {
grid-column: 1 / span 2;
grid-row: 2 / span 2;
}
.magic-bento-card:nth-child(6) {
grid-column: 4;
grid-row: 3;
}
}
/* Border glow effect */
.magic-bento-card--border-glow::after {
content: "";
position: absolute;
inset: 0;
padding: 6px;
background: radial-gradient(
var(--glow-radius) circle at var(--glow-x) var(--glow-y),
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.8)) 0%,
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,
transparent 60%
);
border-radius: inherit;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
opacity: 1;
transition: opacity 0.3s ease;
z-index: 1;
}
.magic-bento-card--border-glow:hover::after {
opacity: 1;
}
.magic-bento-card--border-glow:hover {
box-shadow:
0 4px 20px rgba(46, 24, 78, 0.4),
0 0 30px var(--purple-glow);
}
.particle-container {
position: relative;
overflow: hidden;
}
.particle::before {
content: "";
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: rgba(132, 0, 255, 0.2);
border-radius: 50%;
z-index: -1;
}
.particle-container:hover {
box-shadow:
0 4px 20px rgba(46, 24, 78, 0.2),
0 0 30px var(--purple-glow);
}
/* Global spotlight styles */
.global-spotlight {
mix-blend-mode: screen;
will-change: transform, opacity;
z-index: 200 !important;
pointer-events: none;
}
.bento-section {
position: relative;
user-select: none;
}
:root {
--hue: 27;
--sat: 69%;
--white: hsl(0, 0%, 100%);
--purple-primary: rgba(132, 0, 255, 1);
--purple-glow: rgba(132, 0, 255, 0.2);
--purple-border: rgba(132, 0, 255, 0.8);
--border-color: #392e4e;
--background-dark: #060010;
color-scheme: light dark;
}
.card-grid {
display: grid;
gap: 0.5em;
padding: 0.75em;
max-width: 54em;
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);
}
.magic-bento-card {
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
aspect-ratio: 4/3;
min-height: 200px;
width: 100%;
max-width: 100%;
padding: 1.25em;
border-radius: 20px;
border: 1px solid var(--border-color);
background: var(--background-dark);
font-weight: 300;
overflow: hidden;
transition: all 0.3s ease;
--glow-x: 50%;
--glow-y: 50%;
--glow-intensity: 0;
--glow-radius: 200px;
}
.magic-bento-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.magic-bento-card__header,
.magic-bento-card__content {
display: flex;
position: relative;
color: var(--white);
}
.magic-bento-card__header {
gap: 0.75em;
justify-content: space-between;
}
.magic-bento-card__content {
flex-direction: column;
}
.magic-bento-card__label {
font-size: 16px;
}
.magic-bento-card__title,
.magic-bento-card__description {
--clamp-title: 1;
--clamp-desc: 2;
}
.magic-bento-card__title {
font-weight: 400;
font-size: 16px;
margin: 0 0 0.25em;
}
.magic-bento-card__description {
font-size: 12px;
line-height: 1.2;
opacity: 0.9;
}
.magic-bento-card--text-autohide .magic-bento-card__title,
.magic-bento-card--text-autohide .magic-bento-card__description {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.magic-bento-card--text-autohide .magic-bento-card__title {
-webkit-line-clamp: var(--clamp-title);
line-clamp: var(--clamp-title);
}
.magic-bento-card--text-autohide .magic-bento-card__description {
-webkit-line-clamp: var(--clamp-desc);
line-clamp: var(--clamp-desc);
}
@media (max-width: 599px) {
.card-grid {
grid-template-columns: 1fr;
width: 90%;
margin: 0 auto;
padding: 0.5em;
}
.magic-bento-card {
width: 100%;
min-height: 180px;
}
}
@media (min-width: 600px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.card-grid {
grid-template-columns: repeat(4, 1fr);
}
.magic-bento-card:nth-child(3) {
grid-column: span 2;
grid-row: span 2;
}
.magic-bento-card:nth-child(4) {
grid-column: 1 / span 2;
grid-row: 2 / span 2;
}
.magic-bento-card:nth-child(6) {
grid-column: 4;
grid-row: 3;
}
}
/* Border glow effect */
.magic-bento-card--border-glow::after {
content: "";
position: absolute;
inset: 0;
padding: 6px;
background: radial-gradient(
var(--glow-radius) circle at var(--glow-x) var(--glow-y),
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.8)) 0%,
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,
transparent 60%
);
border-radius: inherit;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
opacity: 1;
transition: opacity 0.3s ease;
z-index: 1;
}
.magic-bento-card--border-glow:hover::after {
opacity: 1;
}
.magic-bento-card--border-glow:hover {
box-shadow:
0 4px 20px rgba(46, 24, 78, 0.4),
0 0 30px var(--purple-glow);
}
.particle-container {
position: relative;
overflow: hidden;
}
.particle::before {
content: "";
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: rgba(132, 0, 255, 0.2);
border-radius: 50%;
z-index: -1;
}
.particle-container:hover {
box-shadow:
0 4px 20px rgba(46, 24, 78, 0.2),
0 0 30px var(--purple-glow);
}
/* Global spotlight styles */
.global-spotlight {
mix-blend-mode: screen;
will-change: transform, opacity;
z-index: 200 !important;
pointer-events: none;
}
.bento-section {
position: relative;
user-select: none;
}
File diff suppressed because it is too large. Load diff
@@ -1,67 +1,67 @@
"use client";
import { type ComponentPropsWithoutRef, useEffect, useRef } from "react";
import { useInView, useMotionValue, useSpring } from "motion/react";
import { cn } from "@/lib/utils";
interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> {
value: number;
startValue?: number;
direction?: "up" | "down";
delay?: number;
decimalPlaces?: number;
}
export function NumberTicker({
value,
startValue = 0,
direction = "up",
delay = 0,
className,
decimalPlaces = 0,
...props
}: NumberTickerProps) {
const ref = useRef<HTMLSpanElement>(null);
const motionValue = useMotionValue(direction === "down" ? value : startValue);
const springValue = useSpring(motionValue, {
damping: 60,
stiffness: 100,
});
const isInView = useInView(ref, { once: true, margin: "0px" });
useEffect(() => {
if (isInView) {
const timer = setTimeout(() => {
motionValue.set(direction === "down" ? startValue : value);
}, delay * 1000);
return () => clearTimeout(timer);
}
}, [motionValue, isInView, delay, value, direction, startValue]);
useEffect(
() =>
springValue.on("change", (latest) => {
if (ref.current) {
ref.current.textContent = Intl.NumberFormat("en-US", {
minimumFractionDigits: decimalPlaces,
maximumFractionDigits: decimalPlaces,
}).format(Number(latest.toFixed(decimalPlaces)));
}
}),
[springValue, decimalPlaces],
);
return (
<span
ref={ref}
className={cn(
"inline-block tracking-wider text-black tabular-nums dark:text-white",
className,
)}
{...props}
>
{startValue}
</span>
);
}
"use client";
import { type ComponentPropsWithoutRef, useEffect, useRef } from "react";
import { useInView, useMotionValue, useSpring } from "motion/react";
import { cn } from "@/lib/utils";
interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> {
value: number;
startValue?: number;
direction?: "up" | "down";
delay?: number;
decimalPlaces?: number;
}
export function NumberTicker({
value,
startValue = 0,
direction = "up",
delay = 0,
className,
decimalPlaces = 0,
...props
}: NumberTickerProps) {
const ref = useRef<HTMLSpanElement>(null);
const motionValue = useMotionValue(direction === "down" ? value : startValue);
const springValue = useSpring(motionValue, {
damping: 60,
stiffness: 100,
});
const isInView = useInView(ref, { once: true, margin: "0px" });
useEffect(() => {
if (isInView) {
const timer = setTimeout(() => {
motionValue.set(direction === "down" ? startValue : value);
}, delay * 1000);
return () => clearTimeout(timer);
}
}, [motionValue, isInView, delay, value, direction, startValue]);
useEffect(
() =>
springValue.on("change", (latest) => {
if (ref.current) {
ref.current.textContent = Intl.NumberFormat("en-US", {
minimumFractionDigits: decimalPlaces,
maximumFractionDigits: decimalPlaces,
}).format(Number(latest.toFixed(decimalPlaces)));
}
}),
[springValue, decimalPlaces],
);
return (
<span
ref={ref}
className={cn(
"inline-block tracking-wider text-black tabular-nums dark:text-white",
className,
)}
{...props}
>
{startValue}
</span>
);
}
+31 -31
View File
@@ -1,31 +1,31 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
}
export { Progress };
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
}
export { Progress };
+56 -56
View File
@@ -1,56 +1,56 @@
"use client";
import { GripVerticalIcon } from "lucide-react";
import * as React from "react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.Group>) {
return (
<ResizablePrimitive.Group
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className,
)}
{...props}
/>
);
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.Separator
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.Separator>
);
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
"use client";
import { GripVerticalIcon } from "lucide-react";
import * as React from "react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.Group>) {
return (
<ResizablePrimitive.Group
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className,
)}
{...props}
/>
);
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.Separator
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.Separator>
);
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
+58 -58
View File
@@ -1,58 +1,58 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };
+190 -190
View File
@@ -1,190 +1,190 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit cursor-pointer items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit cursor-pointer items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};
+28 -28
View File
@@ -1,28 +1,28 @@
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}
export { Separator };
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}
export { Separator };
+139 -139
View File
@@ -1,139 +1,139 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
+63 -63
View File
@@ -1,63 +1,63 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
interface ShineBorderProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Width of the border in pixels
* @default 1
*/
borderWidth?: number;
/**
* Duration of the animation in seconds
* @default 14
*/
duration?: number;
/**
* Color of the border, can be a single color or an array of colors
* @default "#000000"
*/
shineColor?: string | string[];
}
/**
* Shine Border
*
* An animated background border effect component with configurable properties.
*/
export function ShineBorder({
borderWidth = 1,
duration = 14,
shineColor = "#000000",
className,
style,
...props
}: ShineBorderProps) {
return (
<div
style={
{
"--border-width": `${borderWidth}px`,
"--duration": `${duration}s`,
backgroundImage: `radial-gradient(transparent,transparent, ${
Array.isArray(shineColor) ? shineColor.join(",") : shineColor
},transparent,transparent)`,
backgroundSize: "300% 300%",
mask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
WebkitMask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
WebkitMaskComposite: "xor",
maskComposite: "exclude",
padding: "var(--border-width)",
...style,
} as React.CSSProperties
}
className={cn(
"motion-safe:animate-shine pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position]",
className,
)}
{...props}
/>
);
}
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
interface ShineBorderProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Width of the border in pixels
* @default 1
*/
borderWidth?: number;
/**
* Duration of the animation in seconds
* @default 14
*/
duration?: number;
/**
* Color of the border, can be a single color or an array of colors
* @default "#000000"
*/
shineColor?: string | string[];
}
/**
* Shine Border
*
* An animated background border effect component with configurable properties.
*/
export function ShineBorder({
borderWidth = 1,
duration = 14,
shineColor = "#000000",
className,
style,
...props
}: ShineBorderProps) {
return (
<div
style={
{
"--border-width": `${borderWidth}px`,
"--duration": `${duration}s`,
backgroundImage: `radial-gradient(transparent,transparent, ${
Array.isArray(shineColor) ? shineColor.join(",") : shineColor
},transparent,transparent)`,
backgroundSize: "300% 300%",
mask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
WebkitMask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
WebkitMaskComposite: "xor",
maskComposite: "exclude",
padding: "var(--border-width)",
...style,
} as React.CSSProperties
}
className={cn(
"motion-safe:animate-shine pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position]",
className,
)}
{...props}
/>
);
}
File diff suppressed because it is too large. Load diff
+13 -13
View File
@@ -1,13 +1,13 @@
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
);
}
export { Skeleton };
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
);
}
export { Skeleton };
+40 -40
View File
@@ -1,40 +1,40 @@
"use client";
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react";
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };
"use client";
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react";
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };
@@ -1,33 +1,33 @@
.card-spotlight {
position: relative;
border-radius: 1.5rem;
border: 1px solid #222;
background-color: #111;
padding: 2rem;
overflow: hidden;
--mouse-x: 50%;
--mouse-y: 50%;
--spotlight-color: rgba(255, 255, 255, 0.05);
}
.card-spotlight::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
var(--spotlight-color),
transparent 80%
);
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
}
.card-spotlight:hover::before,
.card-spotlight:focus-within::before {
opacity: 0.6;
}
.card-spotlight {
position: relative;
border-radius: 1.5rem;
border: 1px solid #222;
background-color: #111;
padding: 2rem;
overflow: hidden;
--mouse-x: 50%;
--mouse-y: 50%;
--spotlight-color: rgba(255, 255, 255, 0.05);
}
.card-spotlight::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
var(--spotlight-color),
transparent 80%
);
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
}
.card-spotlight:hover::before,
.card-spotlight:focus-within::before {
opacity: 0.6;
}
@@ -1,49 +1,49 @@
"use client";
import React, { useRef } from "react";
import "./spotlight-card.css";
interface Position {
x: number;
y: number;
}
interface SpotlightCardProps extends React.PropsWithChildren {
className?: string;
spotlightColor?: `rgba(${number}, ${number}, ${number}, ${number})`;
style?: React.CSSProperties;
}
const SpotlightCard: React.FC<SpotlightCardProps> = ({
style,
children,
className = "",
spotlightColor = "rgba(255, 255, 255, 0.25)",
}) => {
const divRef = useRef<HTMLDivElement>(null);
const handleMouseMove: React.MouseEventHandler<HTMLDivElement> = (e) => {
if (!divRef.current) return;
const rect = divRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
divRef.current.style.setProperty("--mouse-x", `${x}px`);
divRef.current.style.setProperty("--mouse-y", `${y}px`);
divRef.current.style.setProperty("--spotlight-color", spotlightColor);
};
return (
<div
ref={divRef}
onMouseMove={handleMouseMove}
className={`card-spotlight ${className}`}
style={style}
>
{children}
</div>
);
};
export default SpotlightCard;
"use client";
import React, { useRef } from "react";
import "./spotlight-card.css";
interface Position {
x: number;
y: number;
}
interface SpotlightCardProps extends React.PropsWithChildren {
className?: string;
spotlightColor?: `rgba(${number}, ${number}, ${number}, ${number})`;
style?: React.CSSProperties;
}
const SpotlightCard: React.FC<SpotlightCardProps> = ({
style,
children,
className = "",
spotlightColor = "rgba(255, 255, 255, 0.25)",
}) => {
const divRef = useRef<HTMLDivElement>(null);
const handleMouseMove: React.MouseEventHandler<HTMLDivElement> = (e) => {
if (!divRef.current) return;
const rect = divRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
divRef.current.style.setProperty("--mouse-x", `${x}px`);
divRef.current.style.setProperty("--mouse-y", `${y}px`);
divRef.current.style.setProperty("--spotlight-color", spotlightColor);
};
return (
<div
ref={divRef}
onMouseMove={handleMouseMove}
className={`card-spotlight ${className}`}
style={style}
>
{children}
</div>
);
};
export default SpotlightCard;
+31 -31
View File
@@ -1,31 +1,31 @@
"use client";
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitive.Root>
);
}
export { Switch };
"use client";
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitive.Root>
);
}
export { Switch };
+91 -91
View File
@@ -1,91 +1,91 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
function Tabs({
className,
orientation = "horizontal",
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
className,
)}
{...props}
/>
);
}
const tabsListVariants = cva(
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
},
);
function TabsList({
className,
variant = "default",
...props
}: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
className,
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
function Tabs({
className,
orientation = "horizontal",
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
className,
)}
{...props}
/>
);
}
const tabsListVariants = cva(
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
},
);
function TabsList({
className,
variant = "default",
...props
}: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
className,
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
+257 -257
View File
@@ -1,257 +1,257 @@
"use client";
import {
Children,
createContext,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { motion, type MotionProps, useInView } from "motion/react";
import { cn } from "@/lib/utils";
interface SequenceContextValue {
completeItem: (index: number) => void;
activeIndex: number;
sequenceStarted: boolean;
}
const SequenceContext = createContext<SequenceContextValue | null>(null);
const useSequence = () => useContext(SequenceContext);
const ItemIndexContext = createContext<number | null>(null);
const useItemIndex = () => useContext(ItemIndexContext);
interface AnimatedSpanProps extends MotionProps {
children: React.ReactNode;
delay?: number;
className?: string;
startOnView?: boolean;
}
export const AnimatedSpan = ({
children,
delay = 0,
className,
startOnView = false,
...props
}: AnimatedSpanProps) => {
const elementRef = useRef<HTMLDivElement | null>(null);
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
});
const sequence = useSequence();
const itemIndex = useItemIndex();
const [hasStarted, setHasStarted] = useState(false);
useEffect(() => {
if (!sequence || itemIndex === null) return;
if (!sequence.sequenceStarted) return;
if (hasStarted) return;
if (sequence.activeIndex === itemIndex) {
setHasStarted(true);
}
}, [sequence?.activeIndex, sequence?.sequenceStarted, hasStarted, itemIndex]);
const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true;
return (
<motion.div
ref={elementRef}
initial={{ opacity: 0, y: -5 }}
animate={shouldAnimate ? { opacity: 1, y: 0 } : { opacity: 0, y: -5 }}
transition={{ duration: 0.3, delay: sequence ? 0 : delay / 1000 }}
className={cn("grid text-sm font-normal tracking-tight", className)}
onAnimationComplete={() => {
if (!sequence) return;
if (itemIndex === null) return;
sequence.completeItem(itemIndex);
}}
{...props}
>
{children}
</motion.div>
);
};
interface TypingAnimationProps extends MotionProps {
children: string;
className?: string;
duration?: number;
delay?: number;
as?: React.ElementType;
startOnView?: boolean;
}
export const TypingAnimation = ({
children,
className,
duration = 60,
delay = 0,
as: Component = "span",
startOnView = true,
...props
}: TypingAnimationProps) => {
if (typeof children !== "string") {
throw new Error("TypingAnimation: children must be a string. Received:");
}
const MotionComponent = useMemo(
() =>
motion.create(Component, {
forwardMotionProps: true,
}),
[Component],
);
const [displayedText, setDisplayedText] = useState<string>("");
const [started, setStarted] = useState(false);
const elementRef = useRef<HTMLElement | null>(null);
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
});
const sequence = useSequence();
const itemIndex = useItemIndex();
useEffect(() => {
if (sequence && itemIndex !== null) {
if (!sequence.sequenceStarted) return;
if (started) return;
if (sequence.activeIndex === itemIndex) {
setStarted(true);
}
return;
}
if (!startOnView) {
const startTimeout = setTimeout(() => setStarted(true), delay);
return () => clearTimeout(startTimeout);
}
if (!isInView) return;
const startTimeout = setTimeout(() => setStarted(true), delay);
return () => clearTimeout(startTimeout);
}, [
delay,
startOnView,
isInView,
started,
sequence?.activeIndex,
sequence?.sequenceStarted,
itemIndex,
]);
useEffect(() => {
if (!started) return;
let i = 0;
const typingEffect = setInterval(() => {
if (i < children.length) {
setDisplayedText(children.substring(0, i + 1));
i++;
} else {
clearInterval(typingEffect);
if (sequence && itemIndex !== null) {
sequence.completeItem(itemIndex);
}
}
}, duration);
return () => {
clearInterval(typingEffect);
};
}, [children, duration, started]);
return (
<MotionComponent
ref={elementRef}
className={cn("text-sm font-normal tracking-tight", className)}
{...props}
>
{displayedText}
</MotionComponent>
);
};
interface TerminalProps {
children: React.ReactNode;
className?: string;
sequence?: boolean;
startOnView?: boolean;
}
export const Terminal = ({
children,
className,
sequence = true,
startOnView = true,
}: TerminalProps) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const isInView = useInView(containerRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
});
const [activeIndex, setActiveIndex] = useState(0);
const sequenceHasStarted = sequence ? !startOnView || isInView : false;
const contextValue = useMemo<SequenceContextValue | null>(() => {
if (!sequence) return null;
return {
completeItem: (index: number) => {
setActiveIndex((current) =>
index === current ? current + 1 : current,
);
},
activeIndex,
sequenceStarted: sequenceHasStarted,
};
}, [sequence, activeIndex, sequenceHasStarted]);
const wrappedChildren = useMemo(() => {
if (!sequence) return children;
const array = Children.toArray(children);
return array.map((child, index) => (
<ItemIndexContext.Provider key={index} value={index}>
{child as React.ReactNode}
</ItemIndexContext.Provider>
));
}, [children, sequence]);
const content = (
<div
ref={containerRef}
className={cn(
"border-border bg-background/25 z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border",
className,
)}
>
<div className="border-border flex flex-col gap-y-2 border-b p-4">
<div className="flex flex-row gap-x-2">
<div className="h-2 w-2 rounded-full bg-red-500"></div>
<div className="h-2 w-2 rounded-full bg-yellow-500"></div>
<div className="h-2 w-2 rounded-full bg-green-500"></div>
</div>
</div>
<pre className="p-4">
<code className="grid gap-y-1 overflow-auto">{wrappedChildren}</code>
</pre>
</div>
);
if (!sequence) return content;
return (
<SequenceContext.Provider value={contextValue}>
{content}
</SequenceContext.Provider>
);
};
"use client";
import {
Children,
createContext,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { motion, type MotionProps, useInView } from "motion/react";
import { cn } from "@/lib/utils";
interface SequenceContextValue {
completeItem: (index: number) => void;
activeIndex: number;
sequenceStarted: boolean;
}
const SequenceContext = createContext<SequenceContextValue | null>(null);
const useSequence = () => useContext(SequenceContext);
const ItemIndexContext = createContext<number | null>(null);
const useItemIndex = () => useContext(ItemIndexContext);
interface AnimatedSpanProps extends MotionProps {
children: React.ReactNode;
delay?: number;
className?: string;
startOnView?: boolean;
}
export const AnimatedSpan = ({
children,
delay = 0,
className,
startOnView = false,
...props
}: AnimatedSpanProps) => {
const elementRef = useRef<HTMLDivElement | null>(null);
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
});
const sequence = useSequence();
const itemIndex = useItemIndex();
const [hasStarted, setHasStarted] = useState(false);
useEffect(() => {
if (!sequence || itemIndex === null) return;
if (!sequence.sequenceStarted) return;
if (hasStarted) return;
if (sequence.activeIndex === itemIndex) {
setHasStarted(true);
}
}, [sequence?.activeIndex, sequence?.sequenceStarted, hasStarted, itemIndex]);
const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true;
return (
<motion.div
ref={elementRef}
initial={{ opacity: 0, y: -5 }}
animate={shouldAnimate ? { opacity: 1, y: 0 } : { opacity: 0, y: -5 }}
transition={{ duration: 0.3, delay: sequence ? 0 : delay / 1000 }}
className={cn("grid text-sm font-normal tracking-tight", className)}
onAnimationComplete={() => {
if (!sequence) return;
if (itemIndex === null) return;
sequence.completeItem(itemIndex);
}}
{...props}
>
{children}
</motion.div>
);
};
interface TypingAnimationProps extends MotionProps {
children: string;
className?: string;
duration?: number;
delay?: number;
as?: React.ElementType;
startOnView?: boolean;
}
export const TypingAnimation = ({
children,
className,
duration = 60,
delay = 0,
as: Component = "span",
startOnView = true,
...props
}: TypingAnimationProps) => {
if (typeof children !== "string") {
throw new Error("TypingAnimation: children must be a string. Received:");
}
const MotionComponent = useMemo(
() =>
motion.create(Component, {
forwardMotionProps: true,
}),
[Component],
);
const [displayedText, setDisplayedText] = useState<string>("");
const [started, setStarted] = useState(false);
const elementRef = useRef<HTMLElement | null>(null);
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
});
const sequence = useSequence();
const itemIndex = useItemIndex();
useEffect(() => {
if (sequence && itemIndex !== null) {
if (!sequence.sequenceStarted) return;
if (started) return;
if (sequence.activeIndex === itemIndex) {
setStarted(true);
}
return;
}
if (!startOnView) {
const startTimeout = setTimeout(() => setStarted(true), delay);
return () => clearTimeout(startTimeout);
}
if (!isInView) return;
const startTimeout = setTimeout(() => setStarted(true), delay);
return () => clearTimeout(startTimeout);
}, [
delay,
startOnView,
isInView,
started,
sequence?.activeIndex,
sequence?.sequenceStarted,
itemIndex,
]);
useEffect(() => {
if (!started) return;
let i = 0;
const typingEffect = setInterval(() => {
if (i < children.length) {
setDisplayedText(children.substring(0, i + 1));
i++;
} else {
clearInterval(typingEffect);
if (sequence && itemIndex !== null) {
sequence.completeItem(itemIndex);
}
}
}, duration);
return () => {
clearInterval(typingEffect);
};
}, [children, duration, started]);
return (
<MotionComponent
ref={elementRef}
className={cn("text-sm font-normal tracking-tight", className)}
{...props}
>
{displayedText}
</MotionComponent>
);
};
interface TerminalProps {
children: React.ReactNode;
className?: string;
sequence?: boolean;
startOnView?: boolean;
}
export const Terminal = ({
children,
className,
sequence = true,
startOnView = true,
}: TerminalProps) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const isInView = useInView(containerRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
});
const [activeIndex, setActiveIndex] = useState(0);
const sequenceHasStarted = sequence ? !startOnView || isInView : false;
const contextValue = useMemo<SequenceContextValue | null>(() => {
if (!sequence) return null;
return {
completeItem: (index: number) => {
setActiveIndex((current) =>
index === current ? current + 1 : current,
);
},
activeIndex,
sequenceStarted: sequenceHasStarted,
};
}, [sequence, activeIndex, sequenceHasStarted]);
const wrappedChildren = useMemo(() => {
if (!sequence) return children;
const array = Children.toArray(children);
return array.map((child, index) => (
<ItemIndexContext.Provider key={index} value={index}>
{child as React.ReactNode}
</ItemIndexContext.Provider>
));
}, [children, sequence]);
const content = (
<div
ref={containerRef}
className={cn(
"border-border bg-background/25 z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border",
className,
)}
>
<div className="border-border flex flex-col gap-y-2 border-b p-4">
<div className="flex flex-row gap-x-2">
<div className="h-2 w-2 rounded-full bg-red-500"></div>
<div className="h-2 w-2 rounded-full bg-yellow-500"></div>
<div className="h-2 w-2 rounded-full bg-green-500"></div>
</div>
</div>
<pre className="p-4">
<code className="grid gap-y-1 overflow-auto">{wrappedChildren}</code>
</pre>
</div>
);
if (!sequence) return content;
return (
<SequenceContext.Provider value={contextValue}>
{content}
</SequenceContext.Provider>
);
};
+18 -18
View File
@@ -1,18 +1,18 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground/60 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...props}
/>
);
}
export { Textarea };
import * as React from "react";
import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground/60 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...props}
/>
);
}
export { Textarea };
Loaded 100 of 598 files, more files were not shown because too many files have changed in this diff. Show more