Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb2edeadb4 | ||
|
|
309c864096 | ||
|
|
7a5753b3ed | ||
|
|
3e8b9cf2d9 | ||
|
|
d23d871878 | ||
|
|
3f2aefdcd8 |
No files matched your search
@@ -1 +0,0 @@
|
||||
remote connectivity test - 2026-05-13
|
||||
+56
-65
@@ -1,65 +1,56 @@
|
||||
# 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
|
||||
# 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
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?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>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?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>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,672 @@
|
||||
# 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/`
|
||||
@@ -0,0 +1,4 @@
|
||||
# 本地开发:直连 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
|
||||
@@ -0,0 +1,7 @@
|
||||
# 复制为 .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
|
||||
Whitespace-only changes.
@@ -1,140 +0,0 @@
|
||||
# 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`,减少硬编码路径。
|
||||
@@ -1,975 +0,0 @@
|
||||
# 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
@@ -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>CM 助手</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>DeerFlow Vite</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+8920
-8920
File diff suppressed because it is too large.
Load diff
+95
-96
@@ -1,96 +1,95 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
+107
-2
@@ -3,14 +3,27 @@ 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 PageRoutes from "./pages/PageRoutes";
|
||||
import ScheduledTaskRunDetailPage from "./pages/ScheduledTaskRunDetailPage";
|
||||
import ScheduledTasksPage from "./pages/ScheduledTasksPage";
|
||||
import { NotFoundPage } from "./pages/NotFoundPage";
|
||||
|
||||
function AppProviders({ children }: { children: React.ReactNode }) {
|
||||
@@ -32,6 +45,27 @@ 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>
|
||||
@@ -41,7 +75,78 @@ export function App() {
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/login/:username" element={<LoginPage />} />
|
||||
<Route path="/landing" element={<LandingPage />} />
|
||||
<Route path="/page/*" element={<PageRoutes />} />
|
||||
<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="*" 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>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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">
|
||||
"Originated from Open Source, give back to Open Source."
|
||||
</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>© {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">
|
||||
"Originated from Open Source, give back to Open Source."
|
||||
</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>© {year} CM业务中心-问答管理</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +1,127 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
"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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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,20 +1,35 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -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 CM Assistant a "computer", 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 DeerFlow a "computer", 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's needed, when
|
||||
it'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>
|
||||
);
|
||||
}
|
||||
"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's needed, when
|
||||
it'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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,387 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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";
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
.galaxy-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.galaxy-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
Reference in new issue
Block a user