Compare commits

..
2 Commits
Author SHA1 Message Date
gozeros b638b541d0 初始化 2026-09-07 18:24:55 +08:00
wsk 34474bd5fc Initial commit 2026-09-07 17:37:11 +08:00
864 changed files with 91689 additions and 126209 deletions

No files matched your search

-196
View File
@@ -1,196 +0,0 @@
# Workspace/editor
.vscode/*
!.vscode/launch.json
!.vscode/tasks.json
!.vscode/extensions.json
!.vscode/settings.json
frontend-web/.vscode/
node_modules/
# Claude Code local-only settings (per-user permissions, not shared)
.claude/settings.local.json
# Local runtime logs and pid files
.runtime/
logs/
offline-backend-20260512/backend/logs/
# Frontend local/runtime artifacts
frontend-web/.env
frontend-web/.env.local
frontend-web/.env.*.local
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-web/dist
# Frontend build artifacts
frontend-web/dist/
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.example
offline-backend-20260512/.env
offline-backend-20260512/DEPLOY_BACKEND_OFFLINE_ZH.md
offline-backend-20260512/SHA256SUMS
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/backend/.deer-flow/*
offline-backend-20260512/backend/.env
offline-backend-20260512/backend/.env.local
offline-backend-20260512/start_backend.sh
offline-backend-20260512/stop_backend.sh
offline-backend-20260512/check_backend.sh
hermes-agent-main/
skills/*
offline-backend-20260512/backend/.deer-flow/*
# Per-user skill_evolution overrides — runtime data written by the API
offline-backend-20260512/backend/skill_evolution.json
skills/
hermes-agent-main/
.gitignore
offline-backend-20260512/backend/tests/*
# Keep the openGauss URL/dialect/schema compatibility regression coverage versioned.
!offline-backend-20260512/backend/tests/test_gauss_database.py
!offline-backend-20260512/backend/tests/test_llmwiki_weknora.py
!offline-backend-20260512/backend/tests/test_llmwiki_knowledge_management.py
!offline-backend-20260512/backend/tests/test_health_auth_bypass.py
# Keep formal Word OOXML and embedded-font regression coverage versioned.
!offline-backend-20260512/backend/tests/test_word_export.py
# Keep generated-file library aggregation and filtering regression coverage versioned.
!offline-backend-20260512/backend/tests/test_artifact_library.py
# Keep the model configuration regression coverage versioned even though
# this deployment repository otherwise ignores newly added backend tests.
!offline-backend-20260512/backend/tests/test_model_management.py
# Keep vLLM reasoning/reasoning_content field compatibility regressions versioned.
!offline-backend-20260512/backend/tests/test_reasoning_fields.py
# Keep the forced-research agent runtime gate and sandbox-policy regressions versioned.
!offline-backend-20260512/backend/tests/test_forced_research_agent.py
# Keep strategy-selection built-in agent seed and safety-contract regressions versioned.
!offline-backend-20260512/backend/tests/test_strategy_selection_seed.py
# Keep fixed-question exact-match and streaming regression coverage versioned.
!offline-backend-20260512/backend/tests/test_fixed_questions.py
# Keep configurable es_query system-prompt routing regression coverage versioned.
!offline-backend-20260512/backend/tests/test_es_query_routing_prompt.py
# Keep the local LLMWiki vector-index and external API regression coverage versioned.
!offline-backend-20260512/backend/tests/test_llmwiki_local_index_repository.py
!offline-backend-20260512/backend/tests/test_llmwiki_vector_search.py
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_sync.py
!offline-backend-20260512/backend/tests/test_llmwiki_encoder_runtime.py
!offline-backend-20260512/backend/tests/test_external_llmwiki_search.py
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_retrieval.py
!offline-backend-20260512/backend/tests/test_public_llmwiki_embed.py
# Deep-research reliability regression coverage.
!offline-backend-20260512/backend/tests/test_deep_research_persistence.py
!offline-backend-20260512/backend/tests/test_deep_research_event_sink.py
!offline-backend-20260512/backend/tests/test_deep_research_executor_reliability.py
!offline-backend-20260512/backend/tests/test_deep_research_start_job.py
!offline-backend-20260512/backend/tests/test_summarization_human_message.py
!offline-backend-20260512/backend/tests/test_compact_menu_overrides.py
# Independent enterprise-research persistence, HTTP and race regressions.
!offline-backend-20260512/backend/tests/test_enterprise_research_tasks.py
# Position-roundtable per-turn write/present cap (weak-model duplicate artifacts).
!offline-backend-20260512/backend/tests/test_position_artifact_cap.py
# Roundtable coordinator/seat capability isolation regressions.
!offline-backend-20260512/backend/tests/test_roundtable_role_boundary.py
# Workflow Studio: schemas / definitions / engine / run-lifecycle regressions
# plus the concurrency remediation suite (atomic resume, draft CAS, publish
# counter, event seq) — required in CI per docs/WORKFLOW_STUDIO_BACKEND_REMEDIATION_ZH.md P1-06.
!offline-backend-20260512/backend/tests/test_workflow_schemas.py
!offline-backend-20260512/backend/tests/test_workflow_definitions.py
!offline-backend-20260512/backend/tests/test_workflow_engine.py
!offline-backend-20260512/backend/tests/test_workflow_runs.py
!offline-backend-20260512/backend/tests/test_workflow_concurrency.py
!offline-backend-20260512/backend/tests/test_workflow_schema_rebuild.py
# Writing-mode first-report pipeline enforcement regressions.
!offline-backend-20260512/backend/tests/test_writing_mode_report_middleware.py
!offline-backend-20260512/backend/tests/test_writing_rewrite_prompt.py
# LLM call timeout retry classification regression coverage.
!offline-backend-20260512/backend/tests/test_llm_error_handling_middleware.py
# Writing-mode pipeline conversation-material regressions.
!offline-backend-20260512/backend/tests/test_deep_research_report_tool.py
# Report collaboration (RC-BE-000–018) regression coverage.
!offline-backend-20260512/backend/tests/test_report_collaboration_*.py
!offline-backend-20260512/backend/tests/test_ensure_orm_columns_sync.py
deploy
.history
# Cloned third-party repos (local only)
git-clone/
# Repo-root offline dependency pack (local only)
/dist/
# Frontend offline pack working directories
frontend-web/.offline-pack-build/
frontend-web/.offline-extract-test/
frontend-web-node_modules-offline-*.tar.gz
# Local dependency caches and backend scratch archives
/.pnpm-store/
/offline-backend-20260512/backend/backend*.zip
# Local offline bundles and scratch tooling
/*.tar.gz
/offline-backend-20260512/*.zip
/tmp/
.codegraph
codegraph.mdc
docs/aidev-docs
docs/backups
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
!frontend-web/src/components/workspace/skills/
# uv lockfile (not tracked per project preference)
offline-backend-20260512/backend/uv.lock
!offline-backend-20260512/backend/tests/test_wiki_package_roundtrip.py
# Local-only scratch directory (not committed)
0622/
# Python bytecode cache
__pycache__/
# misc
.DS_Store
+1 -1
View File
@@ -96,7 +96,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
- **智能体使用引导 (Agent onboarding tour, 三段式)**: 智能体「浏览管理 → 创建与配置 → 发起问答」全流程分步引导,组件 `components/workspace/agents/agent-guide.tsx`(同一 TDesign `Guide` 封装模式,与简洁模式引导共享记忆语义:**仅显式点「完成/跳过」才写 localStorage** `deerflow.agent-guide.<stage>.finished`,中途刷新/离开不记忆)。**自动演示仅 create / chat 两段**(进智能体列表页不弹引导,点「创建智能体」进入新建页时才自动演示;list 段的 `AgentGuide` 传 `autoStart={false}`,只能由头部「使用引导」按钮手动触发)。三段各自挂载、各自记忆:① `list`(`agent-gallery.tsx`,/page/workspace/agents)——分区 Tabs、搜索排序、智能体卡片操作、右上「创建智能体」按钮;**完成/跳过就地结束(不跳转创建页,与 create 段无联动)**,最后一步仅提示进入创建页后会有配置引导;② `create`(`NewAgentPage.tsx`,/page/workspace/agents/new)——左侧对话式创建输入区、基本信息、运行模型+入库模板名、知识空间、SOUL.md、技能穿梭框、保存按钮(9 步);③ `chat`(`AgentChatPage.tsx`,agents/:id/chats)——左栏切换与历史、常用问题 chips、输入框(落地态/会话态两个挂点同一 `chat-input` 标记)、右侧详情面板;`eligible` 限定普通智能体对话页(`showAgentList && !isEmbedded && !taskCtxOn`,即排除通用问答原地嵌入与任务深链工作区),三段共用 `isAgentGuideEnvironment()` 排除 embed/hideChrome/无界环境。高亮锚点 `data-agent-guide`:列表页 `tabs`/`search`/`agent-card`(卡片+列表两种布局的根元素)/`new-agent-btn`,创建页 `create-chat-input`/`create-basic`/`create-model`/`create-knowledge`/`create-soul`/`create-skills`/`create-save-btn`,对话页 `chat-sidebar`/`chat-questions`/`chat-input`/`chat-detail`;同样**启动时按目标存在性过滤步骤**(新用户无卡片/无预设问题时自动剔除)。手动重放:三个页面头部各有「使用引导」按钮(列表页创建按钮左侧、创建页 header 右侧、对话页 header),广播 `agent-guide:start:<stage>` 窗口事件强制重放(忽略已看过记忆)。 - **智能体使用引导 (Agent onboarding tour, 三段式)**: 智能体「浏览管理 → 创建与配置 → 发起问答」全流程分步引导,组件 `components/workspace/agents/agent-guide.tsx`(同一 TDesign `Guide` 封装模式,与简洁模式引导共享记忆语义:**仅显式点「完成/跳过」才写 localStorage** `deerflow.agent-guide.<stage>.finished`,中途刷新/离开不记忆)。**自动演示仅 create / chat 两段**(进智能体列表页不弹引导,点「创建智能体」进入新建页时才自动演示;list 段的 `AgentGuide` 传 `autoStart={false}`,只能由头部「使用引导」按钮手动触发)。三段各自挂载、各自记忆:① `list`(`agent-gallery.tsx`,/page/workspace/agents)——分区 Tabs、搜索排序、智能体卡片操作、右上「创建智能体」按钮;**完成/跳过就地结束(不跳转创建页,与 create 段无联动)**,最后一步仅提示进入创建页后会有配置引导;② `create`(`NewAgentPage.tsx`,/page/workspace/agents/new)——左侧对话式创建输入区、基本信息、运行模型+入库模板名、知识空间、SOUL.md、技能穿梭框、保存按钮(9 步);③ `chat`(`AgentChatPage.tsx`,agents/:id/chats)——左栏切换与历史、常用问题 chips、输入框(落地态/会话态两个挂点同一 `chat-input` 标记)、右侧详情面板;`eligible` 限定普通智能体对话页(`showAgentList && !isEmbedded && !taskCtxOn`,即排除通用问答原地嵌入与任务深链工作区),三段共用 `isAgentGuideEnvironment()` 排除 embed/hideChrome/无界环境。高亮锚点 `data-agent-guide`:列表页 `tabs`/`search`/`agent-card`(卡片+列表两种布局的根元素)/`new-agent-btn`,创建页 `create-chat-input`/`create-basic`/`create-model`/`create-knowledge`/`create-soul`/`create-skills`/`create-save-btn`,对话页 `chat-sidebar`/`chat-questions`/`chat-input`/`chat-detail`;同样**启动时按目标存在性过滤步骤**(新用户无卡片/无预设问题时自动剔除)。手动重放:三个页面头部各有「使用引导」按钮(列表页创建按钮左侧、创建页 header 右侧、对话页 header),广播 `agent-guide:start:<stage>` 窗口事件强制重放(忽略已看过记忆)。
- **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape. **侧边栏 + 导航栏 menu labels are desensitized** at the data source: `useSidebarMenuItems`/`useActiveThirdLevel` (`core/page-layout/use-sidebar-menu.ts`) map every node's `label` through a desensitizer built **directly from the `/api/sensitive-words` query** (`makeDesensitizer(wordsToMap(words))`, falling back to the global seed `desensitize` while empty) — this sidesteps the `replaceDesensitizeMap` effect-timing race. Only `label` is rewritten; `path`/`id`/`icon` are untouched so routing/highlight are unaffected. All nav surfaces (`page-sidebar.tsx`, `page-sidebar-dark.tsx`, `page-sidebar-v1.tsx`, `strategy-components/Header.tsx`, `PageRoutes.tsx`) consume this hook and are covered; `MenuManagementPage` uses the raw tree so admins still edit real labels. - **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape. **侧边栏 + 导航栏 menu labels are desensitized** at the data source: `useSidebarMenuItems`/`useActiveThirdLevel` (`core/page-layout/use-sidebar-menu.ts`) map every node's `label` through a desensitizer built **directly from the `/api/sensitive-words` query** (`makeDesensitizer(wordsToMap(words))`, falling back to the global seed `desensitize` while empty) — this sidesteps the `replaceDesensitizeMap` effect-timing race. Only `label` is rewritten; `path`/`id`/`icon` are untouched so routing/highlight are unaffected. All nav surfaces (`page-sidebar.tsx`, `page-sidebar-dark.tsx`, `page-sidebar-v1.tsx`, `strategy-components/Header.tsx`, `PageRoutes.tsx`) consume this hook and are covered; `MenuManagementPage` uses the raw tree so admins still edit real labels.
- **技能地址管理 (Skill Address Management, admin-only)**: 扫描运行中的技能(`skills/{public,custom}` 下所有 `.md`/`.py`)里的 **http(s) URL 与 IP/IP:端口**,按唯一地址聚合全部出现位置(跨 .md/.py),支持**批量替换**(如把 UAT 地址一次性改成生产)。主体抽成可复用组件 `SkillAddressesPanel`(`pages/SkillAddressesPage.tsx` 导出,默认导出 `SkillAddressesPage` 仍保留旧路由 `/page/strategy/admin/skill-addresses` 作直链回退),**作为「技能地址管理」标签页嵌入管理员「技能管理」页 `pages/AdminSkillsPage.tsx`**(该页加 Tabs:「技能列表」+「技能地址管理」;入口在「设置和更多」下拉的「技能管理」`workspace-nav-menu.tsx` → `/page/workspace/admin/skills`,不再单列于 `components/page-sidebar.tsx` 左下角设置下拉):打开自动扫描并带**进度条**(扫描逐技能 / 应用逐文件 X/N,应对 150+ 技能),URL/IP 类型过滤 + 搜索 + 「片段批量套用」(如所有 `uat.4.cn` → `prod.4.cn`)、每地址「替换为」输入框(可展开看 `file:line` 片段)、内联**预览**、**应用**、**多关键词检索**(空格分隔全部命中),以及**修改记录**面板——每次应用生成一条记录(显示时间/改了哪些地址 from→to/文件数/处数),可**针对单次记录回退**(二次确认)。client + NDJSON 流式解析在 `strategy-components/api/skill-addresses.ts`。后端 `/api/skill-addresses`(**仅管理员**):所有文件 IO 走 `asyncio.to_thread` 不阻塞事件循环;`GET /scan`(+`/scan/stream`)、`POST /preview`、`POST /apply`(+`/apply/stream`)、`GET /backups`、`POST /rollback/{id}`;**子串安全替换**(基于 token span,不会误伤子串)、`expected_total` 乐观锁(409)、写前自动磁盘备份到 `{skills_root}/.address-edits/`(同时写一份 `{id}.manifest.json` 记录本次替换明细,供修改记录展示)、应用后刷新技能系统提示缓存。纯引擎 `deerflow/skills/address_scan.py`,router `app/gateway/routers/skill_addresses.py`,tests `tests/test_skill_addresses.py`。范围仅运行中技能,不动 `deploy/minimal/skills/`。 - **技能地址管理 (Skill Address Management, admin-only)**: 扫描运行中的技能(`skills/{public,custom}` 下所有 `.md`/`.py`)里的 **http(s) URL 与 IP/IP:端口**,按唯一地址聚合全部出现位置(跨 .md/.py),支持**批量替换**(如把 UAT 地址一次性改成生产)。主体抽成可复用组件 `SkillAddressesPanel`(`pages/SkillAddressesPage.tsx` 导出,默认导出 `SkillAddressesPage` 仍保留旧路由 `/page/strategy/admin/skill-addresses` 作直链回退),**作为「技能地址管理」标签页嵌入管理员「技能管理」页 `pages/AdminSkillsPage.tsx`**(该页加 Tabs:「技能列表」+「技能地址管理」;入口在「设置和更多」下拉的「技能管理」`workspace-nav-menu.tsx` → `/page/workspace/admin/skills`,不再单列于 `components/page-sidebar.tsx` 左下角设置下拉):打开自动扫描并带**进度条**(扫描逐技能 / 应用逐文件 X/N,应对 150+ 技能),URL/IP 类型过滤 + 搜索 + 「片段批量套用」(如所有 `uat.4.cn` → `prod.4.cn`)、每地址「替换为」输入框(可展开看 `file:line` 片段)、内联**预览**、**应用**、**多关键词检索**(空格分隔全部命中),以及**修改记录**面板——每次应用生成一条记录(显示时间/改了哪些地址 from→to/文件数/处数),可**针对单次记录回退**(二次确认)。client + NDJSON 流式解析在 `strategy-components/api/skill-addresses.ts`。后端 `/api/skill-addresses`(**仅管理员**):所有文件 IO 走 `asyncio.to_thread` 不阻塞事件循环;`GET /scan`(+`/scan/stream`)、`POST /preview`、`POST /apply`(+`/apply/stream`)、`GET /backups`、`POST /rollback/{id}`;**子串安全替换**(基于 token span,不会误伤子串)、`expected_total` 乐观锁(409)、写前自动磁盘备份到 `{skills_root}/.address-edits/`(同时写一份 `{id}.manifest.json` 记录本次替换明细,供修改记录展示)、应用后刷新技能系统提示缓存。纯引擎 `deerflow/skills/address_scan.py`,router `app/gateway/routers/skill_addresses.py`,tests `tests/test_skill_addresses.py`。范围仅运行中技能,不动 `deploy/minimal/skills/`。
- **按钮管理 (Task Button Management, admin-only)**: configures the **可配置跳转按钮** rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see `TaskFxWorkspace`). Each button = `{business, label, linkType, target, appendTaskId, idParam, idKind, enabled, sortOrder}`; `linkType` is `url` (外链 `window.open`, optional append of `?{idParam}={id}` — **`idParam` is the editable query-param name**, e.g. `id`/`task_id`), `business` (内部切换到另一业务, reuses `resolveTaskDeeplinkTarget`, always task id), or `purpose` (弹出当前任务的「任务目的」表格 via the shared `TaskPurposeDialog`, no target — configurable for any page incl. xdfx). **`host`(宿主路由)** asks the embedding parent to navigate: `runTaskButton` sends `window.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin)` — `target` is the host route, `appendTaskId`/`idParam` become numeric `queryParams` (task id, or action id when `idKind:"action"` — same selector as url buttons, xdfx only), origin from `runtime-config.js` `VITE_HOST_NAVIGATE_ORIGIN` (else ancestorOrigins/referrer; never `*`). **`idKind` (`task`/`action`) only matters for xdfx**: its deep-link `?taskId=` is an 行动id and the 任务id is resolved at login (`fetchTaskIdFromAction`) — `TaskContext` now also stashes the raw `actionId`, so a url **or host** button can append either (default `task`). Page `TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, **admin-gated**, entry「按钮管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the **whole set atomically** (replace-all), plus 「导入默认」 to merge code-seed defaults. **Seed/fallback** (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back to `defaultTaskButtons(cfg)` — rwfx's url buttons (编辑/下一步/3q详情) built from the **live** `/api/public/config/task-deeplink` plus cross-business switch buttons for **all three** businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. The old hardcoded jump buttons were migrated out: `TaskFxWorkspace` no longer hardcodes 切换到三情分析, `TaskActionsPanel` keeps only the non-jump 清除记录, and `TaskPurposePanel` dropped its 3q详情 button (now configurable) **and defaults the 目的树 table to collapsed** (chevron toggle). Helpers `core/auth/task-buttons.ts` (`TaskButton`, `defaultTaskButtons`, `resolveTaskButtons`, `buttonsForBusiness`, `runTaskButton`), client+hooks `strategy-components/api/task-buttons.ts`. Backend: `/api/task-buttons` (global/shared; open read, admin replace-all), store `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`), wired `app.state.task_button_store`. Tests: `tests/test_task_buttons.py`. - **按钮管理 (Task Button Management, admin-only)**: configures the **可配置跳转按钮** rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see `TaskFxWorkspace`). Each button = `{business, label, linkType, target, appendTaskId, idParam, idKind, enabled, sortOrder}`; `linkType` is `url` (外链 `window.open`, optional append of `?{idParam}={id}` — **`idParam` is the editable query-param name**, e.g. `id`/`task_id`), `business` (内部切换到另一业务, reuses `resolveTaskDeeplinkTarget`, always task id), or `purpose` (弹出当前任务的「任务目的」表格 via the shared `TaskPurposeDialog`, no target — configurable for any page incl. xdfx). **`host`(宿主路由)** asks the embedding parent to navigate: `runTaskButton` sends `window.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin)` — `target` is the host route, `appendTaskId`/`idParam` become numeric `queryParams` (task id only), origin from `runtime-config.js` `VITE_HOST_NAVIGATE_ORIGIN` (else ancestorOrigins/referrer; never `*`). **`idKind` (`task`/`action`) only matters for xdfx**: its deep-link `?taskId=` is an 行动id and the 任务id is resolved at login (`fetchTaskIdFromAction`) — `TaskContext` now also stashes the raw `actionId`, so a url button can append either (default `task`). Page `TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, **admin-gated**, entry「按钮管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the **whole set atomically** (replace-all), plus 「导入默认」 to merge code-seed defaults. **Seed/fallback** (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back to `defaultTaskButtons(cfg)` — rwfx's url buttons (编辑/下一步/3q详情) built from the **live** `/api/public/config/task-deeplink` plus cross-business switch buttons for **all three** businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. The old hardcoded jump buttons were migrated out: `TaskFxWorkspace` no longer hardcodes 切换到三情分析, `TaskActionsPanel` keeps only the non-jump 清除记录, and `TaskPurposePanel` dropped its 3q详情 button (now configurable) **and defaults the 目的树 table to collapsed** (chevron toggle). Helpers `core/auth/task-buttons.ts` (`TaskButton`, `defaultTaskButtons`, `resolveTaskButtons`, `buttonsForBusiness`, `runTaskButton`), client+hooks `strategy-components/api/task-buttons.ts`. Backend: `/api/task-buttons` (global/shared; open read, admin replace-all), store `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`), wired `app.state.task_button_store`. Tests: `tests/test_task_buttons.py`.
- **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes` - **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes`
- **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE - **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/` - **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
-1
View File
@@ -83,7 +83,6 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
- **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps) - **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps)
- **定时任务「JSON → HTML 模板填充」(模板数据转换器)**: a third scheduled-task execution mode. In the create/edit dialog (`ScheduledTasksPage.tsx`), selecting the built-in agent **「模板数据转换器」** (`execution_agent_name === "template-json-builder"`, `TEMPLATE_BUILDER_AGENT_ID`) reveals a 「HTML模板」 select (`useHtmlTemplates` → `GET /api/scheduled-tasks/html-templates`) + a **.json 上传** button (reads the file's text into the prompt box) and **hides** the 必须生成 HTML/Markdown switches (this mode always outputs HTML). The chosen template persists as `execution_context.template_config.template_id` (`TemplateFillConfig` in `core/scheduled-tasks/types.ts`). At run time the backend converts the source JSON (the task prompt) into that template's `DATA` structure, validates (retry-on-fail), fills the template, and stores an HTML page previewable via the existing run-detail HTML preview. The source can be **pure JSON, plain text, or text-with-embedded-JSON** — the backend pre-extracts any embedded JSON and the agent's `SOUL.md` maps it (or the plain text's semantics) to the template's schema skeleton. **Chat-page debug entry (only this agent's `AgentChatPage`)**: when `agent_id === "template-json-builder"`, the chat header shows a 「页面模板」 select + a 「渲染预览」 button — after a normal Q&A run, the button takes the **latest AI reply**, POSTs it to `POST /api/scheduled-tasks/template-fill/render` (`renderTemplateFill`), and pops a dialog rendering the filled HTML in a `sandbox="allow-scripts"` iframe plus the structure-validation status/errors/gaps — for debugging the agent's conversion effect without creating a scheduled task. Gated strictly on the agent id, so other agents' chat pages are untouched. See the backend CLAUDE.md「Template-fill mode」for the engine (`deerflow/runtime/scheduler/template_fill.py`), agent seed, dispatch, and the render API. - **定时任务「JSON → HTML 模板填充」(模板数据转换器)**: a third scheduled-task execution mode. In the create/edit dialog (`ScheduledTasksPage.tsx`), selecting the built-in agent **「模板数据转换器」** (`execution_agent_name === "template-json-builder"`, `TEMPLATE_BUILDER_AGENT_ID`) reveals a 「HTML模板」 select (`useHtmlTemplates` → `GET /api/scheduled-tasks/html-templates`) + a **.json 上传** button (reads the file's text into the prompt box) and **hides** the 必须生成 HTML/Markdown switches (this mode always outputs HTML). The chosen template persists as `execution_context.template_config.template_id` (`TemplateFillConfig` in `core/scheduled-tasks/types.ts`). At run time the backend converts the source JSON (the task prompt) into that template's `DATA` structure, validates (retry-on-fail), fills the template, and stores an HTML page previewable via the existing run-detail HTML preview. The source can be **pure JSON, plain text, or text-with-embedded-JSON** — the backend pre-extracts any embedded JSON and the agent's `SOUL.md` maps it (or the plain text's semantics) to the template's schema skeleton. **Chat-page debug entry (only this agent's `AgentChatPage`)**: when `agent_id === "template-json-builder"`, the chat header shows a 「页面模板」 select + a 「渲染预览」 button — after a normal Q&A run, the button takes the **latest AI reply**, POSTs it to `POST /api/scheduled-tasks/template-fill/render` (`renderTemplateFill`), and pops a dialog rendering the filled HTML in a `sandbox="allow-scripts"` iframe plus the structure-validation status/errors/gaps — for debugging the agent's conversion effect without creating a scheduled task. Gated strictly on the agent id, so other agents' chat pages are untouched. See the backend CLAUDE.md「Template-fill mode」for the engine (`deerflow/runtime/scheduler/template_fill.py`), agent seed, dispatch, and the render API.
- **AI 写作·对话版(对话驱动写作台)**: a **parallel new entry** beside the classic AI-writing page (classic page/routes untouched) — route `/page/canvas/ai-writing-chat` (`OpenCanvasRoutes.tsx`), sidebar 「问答管理 → 课题研究(对话版)」. Page `open-canvas/pages/AIWritingChatPage.tsx` fully **reuses the engine** (`AIWritingContext` state machine + LangGraph `useStream`, `AIWritingHistoryProvider`, right-side `AIWritingDraftPanel`) but replaces the presentation: left = `components/ai-writing-chat/WritingChatPanel.tsx` (timeline + **button-only intervention cards** `ChatInterventionCards.tsx` — the five pause cards copied from `InterventionCard.tsx` with all inline textareas removed, confirm/action buttons kept — plus a bottom **unified smart composer**). Free text goes through two-level intent resolution (`open-canvas/api/ai-writing-intent.ts`): deterministic short-phrase fast path (确认/继续/重新搜索/重写大纲…; high-cost actions never fast-pathed) then `POST /api/ai-writing/sessions/{id}/intent` — the backend reads the graph checkpoint (authoritative pause point from the pending interrupt) and routes via `deerflow/agents/ai_writing/intent_router.py` (LLM + strict per-pause action whitelist aligned to the graph's real resume actions, confidence gates: finalize/force_finalize/覆盖式 re_search need ≥0.85 else clarify, payload key-whitelist cleaning). Results map to `submitIntervention`/control (暂停/继续/后台挂起)/QA bypass (`answer` rendered as a chat bubble, never touches graph state)/clarify (amber follow-up bubble with multi-turn context). **Two-state layout**: idle (未开始写作) renders the **classic setup experience unchanged** — `WritingSetupChat` + collapsible `AIWritingPanel` form under `WritingFormProvider` (same as `AIWritingPage`'s idle branch); once writing starts (or history view opens) it switches to the chat workbench. **Unified chronological feed** (`ChatFlowTimeline.tsx`): intent-layer chat bubbles are interleaved INTO the progress-event timeline by anchor (= `progressEvents.length` at message creation), so events triggered by a user message render **below** it (Google-AI-Studio-style single stream, not timeline-above/chat-below); composer-driven interventions suppress the duplicate 「我的要求」card (`suppressedRequirementIndices`). **素材不足自动续跑** only at `material_confirm` (the only pause the graph supports re-search from): needResearch caches the payload as a pending intent, submits `re_search + userQuery`, and auto-submits the pending intent when the new material-confirm pause arrives (cancellable pill). Stage chips + per-stage placeholder + 「当前交互对象」status badge. Backend tests `tests/test_ai_writing_intent.py`. Plan doc: `frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md`. - **AI 写作·对话版(对话驱动写作台)**: a **parallel new entry** beside the classic AI-writing page (classic page/routes untouched) — route `/page/canvas/ai-writing-chat` (`OpenCanvasRoutes.tsx`), sidebar 「问答管理 → 课题研究(对话版)」. Page `open-canvas/pages/AIWritingChatPage.tsx` fully **reuses the engine** (`AIWritingContext` state machine + LangGraph `useStream`, `AIWritingHistoryProvider`, right-side `AIWritingDraftPanel`) but replaces the presentation: left = `components/ai-writing-chat/WritingChatPanel.tsx` (timeline + **button-only intervention cards** `ChatInterventionCards.tsx` — the five pause cards copied from `InterventionCard.tsx` with all inline textareas removed, confirm/action buttons kept — plus a bottom **unified smart composer**). Free text goes through two-level intent resolution (`open-canvas/api/ai-writing-intent.ts`): deterministic short-phrase fast path (确认/继续/重新搜索/重写大纲…; high-cost actions never fast-pathed) then `POST /api/ai-writing/sessions/{id}/intent` — the backend reads the graph checkpoint (authoritative pause point from the pending interrupt) and routes via `deerflow/agents/ai_writing/intent_router.py` (LLM + strict per-pause action whitelist aligned to the graph's real resume actions, confidence gates: finalize/force_finalize/覆盖式 re_search need ≥0.85 else clarify, payload key-whitelist cleaning). Results map to `submitIntervention`/control (暂停/继续/后台挂起)/QA bypass (`answer` rendered as a chat bubble, never touches graph state)/clarify (amber follow-up bubble with multi-turn context). **Two-state layout**: idle (未开始写作) renders the **classic setup experience unchanged** — `WritingSetupChat` + collapsible `AIWritingPanel` form under `WritingFormProvider` (same as `AIWritingPage`'s idle branch); once writing starts (or history view opens) it switches to the chat workbench. **Unified chronological feed** (`ChatFlowTimeline.tsx`): intent-layer chat bubbles are interleaved INTO the progress-event timeline by anchor (= `progressEvents.length` at message creation), so events triggered by a user message render **below** it (Google-AI-Studio-style single stream, not timeline-above/chat-below); composer-driven interventions suppress the duplicate 「我的要求」card (`suppressedRequirementIndices`). **素材不足自动续跑** only at `material_confirm` (the only pause the graph supports re-search from): needResearch caches the payload as a pending intent, submits `re_search + userQuery`, and auto-submits the pending intent when the new material-confirm pause arrives (cancellable pill). Stage chips + per-stage placeholder + 「当前交互对象」status badge. Backend tests `tests/test_ai_writing_intent.py`. Plan doc: `frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md`.
- **策略优选**: route `/page/strategy/qa/strategy-selection`, sidebar 「问答管理 → 策略优选」. The standalone `src/strategy-selection/` workbench has two horizontal split stages: Step 1 keeps intent-understanding conversation cards on the left and an editable task draft plus angle selection on the right; Step 2 keeps multiplexed parallel strategy cards and automatic judging on the left while the right stays dedicated to the selected-strategy basket and final Markdown synthesis. It consumes the existing `POST /api/parallel-agents/run/stream` protocol, isolates output by stable agent id, supports expanded per-agent dialogs, user selection, Markdown editing/copy/download, and shared server-side DOCX export. A clearly marked neutral Taiwan-Strait risk-management demo is loaded by default for offline UI acceptance. Administrators can edit the SOUL and skills of the nine built-in functional agents through the page dialog; the backend seeds `strategy-selection-{intent,angle-*,judge,solution}` only when their directories are missing, preserving later admin changes. Pure parsing/SSE tests live in `src/strategy-selection/lib.test.ts`; backend seed tests live in `tests/test_strategy_selection_seed.py`.
- **轻应用中心 (Light App Center)**: a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers `window_open` or `iframe` mini-apps with a route-param builder: login params resolved from `localStorage.userInfo`, theme params literal) and 应用列表 (card grid of `window_open` apps, `window.open` launch). `iframe` apps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and **dynamically injected into the sidebar** by `core/page-layout/use-sidebar-menu.ts` (`useSidebarMenuItems`), opening in the `LightAppIframeView` host. Client + React Query hooks in `strategy-components/api/light-apps.ts`; URL builder/constants in `strategy-components/lib/light-app.ts`. Backend: `/api/light-apps` (global/shared, admin-gated writes). - **轻应用中心 (Light App Center)**: a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers `window_open` or `iframe` mini-apps with a route-param builder: login params resolved from `localStorage.userInfo`, theme params literal) and 应用列表 (card grid of `window_open` apps, `window.open` launch). `iframe` apps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and **dynamically injected into the sidebar** by `core/page-layout/use-sidebar-menu.ts` (`useSidebarMenuItems`), opening in the `LightAppIframeView` host. Client + React Query hooks in `strategy-components/api/light-apps.ts`; URL builder/constants in `strategy-components/lib/light-app.ts`. Backend: `/api/light-apps` (global/shared, admin-gated writes).
- **菜单管理 (Menu Management, admin-only)**: an **overlay** over the static sidebar menu — `MenuManagementPage` (`pages/MenuManagementPage.tsx`, route `/page/strategy/admin/menu-management`, opened from the admin-only 「菜单管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`). Admins rename / 停用·启用 / reorder (↑↓ or @dnd-kit drag) / re-parent (horizontal drag **or** the edit dialog's 「移动到」parent selector) any node across 一级/二级/三级, **including injected iframe light-app nodes** (`light-app-<appId>`). The static tree (`core/page-layout/sidebar-menu.ts`) stays the source of icons/routes; only per-node deltas (`label`/`disabled`/`parentId`/`sortOrder`) persist to the backend. Merge logic in `core/page-layout/menu-overrides.ts` (`applyMenuOverrides`, applied in `use-sidebar-menu.ts` after light-app injection, before role filter; disabling a node hides its whole subtree) — strict **3-level cap** (`wouldExceedDepth`). Client `strategy-components/api/menu-overrides.ts`. Backend: `/api/menu-overrides` (global/shared; open read, admin atomic replace-all). **简介模式** left-nav is a separate overlay: `/api/compact-menu-overrides` stores two independent sets (type `A` / `B`); each website picks one via `runtime-config.js` `VITE_COMPACT_MENU_TYPE`. - **菜单管理 (Menu Management, admin-only)**: an **overlay** over the static sidebar menu — `MenuManagementPage` (`pages/MenuManagementPage.tsx`, route `/page/strategy/admin/menu-management`, opened from the admin-only 「菜单管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`). Admins rename / 停用·启用 / reorder (↑↓ or @dnd-kit drag) / re-parent (horizontal drag **or** the edit dialog's 「移动到」parent selector) any node across 一级/二级/三级, **including injected iframe light-app nodes** (`light-app-<appId>`). The static tree (`core/page-layout/sidebar-menu.ts`) stays the source of icons/routes; only per-node deltas (`label`/`disabled`/`parentId`/`sortOrder`) persist to the backend. Merge logic in `core/page-layout/menu-overrides.ts` (`applyMenuOverrides`, applied in `use-sidebar-menu.ts` after light-app injection, before role filter; disabling a node hides its whole subtree) — strict **3-level cap** (`wouldExceedDepth`). Client `strategy-components/api/menu-overrides.ts`. Backend: `/api/menu-overrides` (global/shared; open read, admin atomic replace-all). **简介模式** left-nav is a separate overlay: `/api/compact-menu-overrides` stores two independent sets (type `A` / `B`); each website picks one via `runtime-config.js` `VITE_COMPACT_MENU_TYPE`.
- **白名单管理 (Login Whitelist, admin-only)**: gates **who may log in**, run as a **blocklist (黑名单模式)**. Two pieces: a global master switch (`system_settings.json → login_whitelist.enabled`, surfaced by **admin-only** `GET|PUT /api/system-settings/login-whitelist`) + a per-user **放行** flag (`users.approved` column, migration `20260625_02`). Users **self-register** as usual and **every new account defaults to `approved=True`** — so a user who has never logged into this system before is allowed straight through; the switch only blocks users an admin has explicitly **取消放行** (`approved=False`). (`create_user`/`User`/`UserRow` all default `approved=True`; admins are always approved; existing rows are backfilled `True`.) Login interception is the single helper `enforce_user_approved(user)` in `app/gateway/routers/auth.py`, called in all three login endpoints (`/login/{local,username,token}`) **after** the account is resolved/created and **before** the JWT is issued: switch off → everyone passes; `system_role=="admin"` → always passes (anti-lockout); else `approved=False` (admin-revoked) → 403 `{code:"PENDING_APPROVAL"}` (the frontend's `extractErrorMessage` surfaces the message and login throws before writing any auth state, so no half-login). Admin approves on `pages/LoginWhitelistPage.tsx` (route `/page/strategy/admin/login-whitelist`, entry「白名单管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both sidebar modes): a top master-switch + a list of all registered users (`GET /api/v1/auth/users`, now carrying `approved`) with a per-row 放行 Switch (`PUT /api/v1/auth/users/{id}/approval`, admin-only; revoking only blocks the **next** login — `token_version` is left untouched, existing sessions are not force-killed). Client/hooks: switch in `strategy-components/api/login-whitelist.ts`, user list + approval in `core/auth/{api,hooks}.ts` (`useSetUserApproval`). Backend tests: `tests/test_login_approval.py`. **账号级「登录口令」(per-account password — 临时开通)**: the same page also lets an admin assign one specific account an individual **plaintext** password (`users.login_password` column, migration `20260625_04`, nullable; auto-added on startup by `_ensure_orm_columns_sync`). Once set, **that account's** username login (`POST /api/v1/auth/login/username`) **requires** `?password=` to exactly match it and that personal password **takes precedence over** the global shared-password gate (`username_login.require_password`); accounts without one keep today's shared-gate / passwordless behavior. The user then logs in via the **existing** frontend URL `#/login/<用户名>?password=xxxx` (LoginPage already forwards `?password=`; `<用户名>` = the account's email `@`-prefix). **Plaintext is intentional** — the password rides in the URL query anyway, so storing it readably lets the admin re-copy the full login link from the page; it is exposed only on the **admin-only** `GET /api/v1/auth/users` (`UserListItem.login_password`) and the new admin-only `PUT /api/v1/auth/users/{id}/login-password` (set non-empty / clear with null·empty; doesn't touch `token_version`). The whitelist page renders a per-user 「登录口令」 column with a set/修改 editor + a live 「复制登录链接」 button (`useSetUserLoginPassword` in `core/auth/hooks.ts`, `setUserLoginPassword` in `core/auth/api.ts`). Backend gate helper `_lookup_user_by_username` (lookup-only, no auto-register) in `routers/auth.py`; tests `tests/test_login_personal_password.py`. - **白名单管理 (Login Whitelist, admin-only)**: gates **who may log in**, run as a **blocklist (黑名单模式)**. Two pieces: a global master switch (`system_settings.json → login_whitelist.enabled`, surfaced by **admin-only** `GET|PUT /api/system-settings/login-whitelist`) + a per-user **放行** flag (`users.approved` column, migration `20260625_02`). Users **self-register** as usual and **every new account defaults to `approved=True`** — so a user who has never logged into this system before is allowed straight through; the switch only blocks users an admin has explicitly **取消放行** (`approved=False`). (`create_user`/`User`/`UserRow` all default `approved=True`; admins are always approved; existing rows are backfilled `True`.) Login interception is the single helper `enforce_user_approved(user)` in `app/gateway/routers/auth.py`, called in all three login endpoints (`/login/{local,username,token}`) **after** the account is resolved/created and **before** the JWT is issued: switch off → everyone passes; `system_role=="admin"` → always passes (anti-lockout); else `approved=False` (admin-revoked) → 403 `{code:"PENDING_APPROVAL"}` (the frontend's `extractErrorMessage` surfaces the message and login throws before writing any auth state, so no half-login). Admin approves on `pages/LoginWhitelistPage.tsx` (route `/page/strategy/admin/login-whitelist`, entry「白名单管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both sidebar modes): a top master-switch + a list of all registered users (`GET /api/v1/auth/users`, now carrying `approved`) with a per-row 放行 Switch (`PUT /api/v1/auth/users/{id}/approval`, admin-only; revoking only blocks the **next** login — `token_version` is left untouched, existing sessions are not force-killed). Client/hooks: switch in `strategy-components/api/login-whitelist.ts`, user list + approval in `core/auth/{api,hooks}.ts` (`useSetUserApproval`). Backend tests: `tests/test_login_approval.py`. **账号级「登录口令」(per-account password — 临时开通)**: the same page also lets an admin assign one specific account an individual **plaintext** password (`users.login_password` column, migration `20260625_04`, nullable; auto-added on startup by `_ensure_orm_columns_sync`). Once set, **that account's** username login (`POST /api/v1/auth/login/username`) **requires** `?password=` to exactly match it and that personal password **takes precedence over** the global shared-password gate (`username_login.require_password`); accounts without one keep today's shared-gate / passwordless behavior. The user then logs in via the **existing** frontend URL `#/login/<用户名>?password=xxxx` (LoginPage already forwards `?password=`; `<用户名>` = the account's email `@`-prefix). **Plaintext is intentional** — the password rides in the URL query anyway, so storing it readably lets the admin re-copy the full login link from the page; it is exposed only on the **admin-only** `GET /api/v1/auth/users` (`UserListItem.login_password`) and the new admin-only `PUT /api/v1/auth/users/{id}/login-password` (set non-empty / clear with null·empty; doesn't touch `token_version`). The whitelist page renders a per-user 「登录口令」 column with a set/修改 editor + a live 「复制登录链接」 button (`useSetUserLoginPassword` in `core/auth/hooks.ts`, `setUserLoginPassword` in `core/auth/api.ts`). Backend gate helper `_lookup_user_by_username` (lookup-only, no auto-register) in `routers/auth.py`; tests `tests/test_login_personal_password.py`.
+3
View File
@@ -0,0 +1,3 @@
# deerflow-code
deerflow-code
Submodule _skills_src/ai-skills deleted from cde1c8385e.
Submodule _skills_src/anthropics-skills deleted from 5754626092.
Submodule _skills_src/awesome-claude-skills deleted from 92568c1eda.
Submodule _skills_src/ce-kit deleted from d4973c4827.
Submodule _skills_src/csm deleted from 3fa16a94e0.
Submodule _skills_src/superpowers deleted from 6fd4507659.
Submodule _skills_src/tapestry-skills deleted from 80e1dc56df.
+1 -1
View File
@@ -18,7 +18,7 @@ VITE_BACKEND_BASE_URL=http://127.0.0.1:8001
VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api
# 开发环境普通登录后进入 Vite 内隔离的官方风格页面;/page/* 仍访问当前定制版。 # 开发环境普通登录后进入 Vite 内隔离的官方风格页面;/page/* 仍访问当前定制版。
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false VITE_USE_OFFICIAL_DEERFLOW_ENTRY=true
# 轻应用「新建笔记」系列接口 # 轻应用「新建笔记」系列接口
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000 VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
-3
View File
@@ -59,9 +59,6 @@
# 简介模式左侧菜单类型:与菜单管理页「简介模式」的类型 A / B 对应。两套网页分别填 A 或 B。 # 简介模式左侧菜单类型:与菜单管理页「简介模式」的类型 A / B 对应。两套网页分别填 A 或 B。
# 不填或留空时按类型 A,继续使用原来的简介菜单数据。 # 不填或留空时按类型 A,继续使用原来的简介菜单数据。
# VITE_COMPACT_MENU_TYPE=A # VITE_COMPACT_MENU_TYPE=A
# 问答页「极速」模式:true=在模式菜单显示并直连大模型;false 或未设=隐藏。
# 部署后优先改 dist/runtime-config.js 的 VITE_TURBO_MODE_VISIBLE,刷新即可。
# VITE_TURBO_MODE_VISIBLE=false
# VITE_GITHUB_OAUTH_TOKEN= # VITE_GITHUB_OAUTH_TOKEN=
# --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)--- # --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)---
+20 -20
View File
@@ -1,25 +1,25 @@
<!doctype html> <!doctype html>
<html lang="en-US" suppressHydrationWarning> <html lang="en-US" suppressHydrationWarning>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<script> <script>
(function () { (function () {
try { try {
var t = localStorage.getItem("strategy-theme"); var t = localStorage.getItem("strategy-theme");
var root = document.documentElement; var root = document.documentElement;
root.classList.remove("dark", "light"); root.classList.remove("dark", "light");
if (t === "light") return; if (t === "light") return;
root.classList.add("dark"); root.classList.add("dark");
} catch (e) {} } catch (e) {}
})(); })();
</script> </script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>CM 助手</title> <title>CM 助手</title>
<script src="/runtime-config.js" data-runtime-config></script> <script src="/runtime-config.js" data-runtime-config></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>
-5
View File
@@ -151,11 +151,6 @@ window.__CM_RUNTIME_CONFIG__ = {
// GitHub OAuth Token;不使用 GitHub 相关能力时留空。 // GitHub OAuth Token;不使用 GitHub 相关能力时留空。
VITE_GITHUB_OAUTH_TOKEN: "", VITE_GITHUB_OAUTH_TOKEN: "",
// 是否在问答输入框「模式」菜单显示「极速」:"true" 显示,"false" 隐藏。
// 默认关闭。开启后可直连大模型流式问答,不走智能体工具与技能。
// 部署后改 dist/runtime-config.js 再刷新页面即可,无需重新打包。
VITE_TURBO_MODE_VISIBLE: "true",
// 是否在智能体管理页显示「舆情分析」类型:"true" 显示,"false" 隐藏。 // 是否在智能体管理页显示「舆情分析」类型:"true" 显示,"false" 隐藏。
// 关闭后,该智能体的专用对话地址也会自动跳回智能体管理页。 // 关闭后,该智能体的专用对话地址也会自动跳回智能体管理页。
VITE_SENTIMENT_AGENT_VISIBLE: "true", VITE_SENTIMENT_AGENT_VISIBLE: "true",
@@ -1,158 +1,158 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { type LucideIcon, XIcon } from "lucide-react"; import { type LucideIcon, XIcon } from "lucide-react";
import { forwardRef, type ComponentProps, type HTMLAttributes } from "react"; import { forwardRef, type ComponentProps, type HTMLAttributes } from "react";
export type ArtifactProps = HTMLAttributes<HTMLDivElement>; export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
export const Artifact = ({ className, ...props }: ArtifactProps) => ( export const Artifact = ({ className, ...props }: ArtifactProps) => (
<div <div
className={cn( className={cn(
"flex flex-col overflow-hidden rounded-[1.35rem] border border-border bg-background shadow-[0_24px_60px_-28px_rgba(15,23,42,0.16)] ring-1 ring-black/3", "flex flex-col overflow-hidden rounded-[1.35rem] border border-border bg-background shadow-[0_24px_60px_-28px_rgba(15,23,42,0.16)] ring-1 ring-black/3",
className, className,
)} )}
{...props} {...props}
/> />
); );
export type ArtifactHeaderProps = HTMLAttributes<HTMLDivElement>; export type ArtifactHeaderProps = HTMLAttributes<HTMLDivElement>;
export const ArtifactHeader = ({ export const ArtifactHeader = ({
className, className,
...props ...props
}: ArtifactHeaderProps) => ( }: ArtifactHeaderProps) => (
<div <div
className={cn( className={cn(
"flex items-center justify-between border-b border-border bg-background px-4 py-3", "flex items-center justify-between border-b border-border bg-background px-4 py-3",
className, className,
)} )}
{...props} {...props}
/> />
); );
export type ArtifactCloseProps = ComponentProps<typeof Button>; export type ArtifactCloseProps = ComponentProps<typeof Button>;
export const ArtifactClose = ({ export const ArtifactClose = ({
className, className,
children, children,
size = "sm", size = "sm",
variant = "ghost", variant = "ghost",
...props ...props
}: ArtifactCloseProps) => ( }: ArtifactCloseProps) => (
<Button <Button
className={cn( className={cn(
"text-muted-foreground hover:text-foreground size-8 p-0", "text-muted-foreground hover:text-foreground size-8 p-0",
className, className,
)} )}
size={size} size={size}
type="button" type="button"
variant={variant} variant={variant}
{...props} {...props}
> >
{children ?? <XIcon className="size-4" />} {children ?? <XIcon className="size-4" />}
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</Button> </Button>
); );
export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>; export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>;
export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => ( export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
<div <div
className={cn( className={cn(
"text-foreground text-sm font-semibold tracking-tight", "text-foreground text-sm font-semibold tracking-tight",
className, className,
)} )}
{...props} {...props}
/> />
); );
export type ArtifactDescriptionProps = HTMLAttributes<HTMLParagraphElement>; export type ArtifactDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
export const ArtifactDescription = ({ export const ArtifactDescription = ({
className, className,
...props ...props
}: ArtifactDescriptionProps) => ( }: ArtifactDescriptionProps) => (
<p className={cn("text-muted-foreground text-sm", className)} {...props} /> <p className={cn("text-muted-foreground text-sm", className)} {...props} />
); );
export type ArtifactActionsProps = HTMLAttributes<HTMLDivElement>; export type ArtifactActionsProps = HTMLAttributes<HTMLDivElement>;
export const ArtifactActions = ({ export const ArtifactActions = ({
className, className,
...props ...props
}: ArtifactActionsProps) => ( }: ArtifactActionsProps) => (
<div className={cn("flex items-center gap-1.5", className)} {...props} /> <div className={cn("flex items-center gap-1.5", className)} {...props} />
); );
export type ArtifactActionProps = ComponentProps<typeof Button> & { export type ArtifactActionProps = ComponentProps<typeof Button> & {
tooltip?: string; tooltip?: string;
label?: string; label?: string;
icon?: LucideIcon; icon?: LucideIcon;
}; };
export const ArtifactAction = ({ export const ArtifactAction = ({
tooltip, tooltip,
label, label,
icon: Icon, icon: Icon,
children, children,
className, className,
size = "sm", size = "sm",
variant = "ghost", variant = "ghost",
...props ...props
}: ArtifactActionProps) => { }: ArtifactActionProps) => {
const button = ( const button = (
<Button <Button
className={cn( 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]", "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, className,
)} )}
size={size} size={size}
type="button" type="button"
variant={variant} variant={variant}
{...props} {...props}
> >
{Icon ? <Icon className="size-4" /> : children} {Icon ? <Icon className="size-4" /> : children}
<span className="sr-only">{label || tooltip}</span> <span className="sr-only">{label || tooltip}</span>
</Button> </Button>
); );
if (tooltip) { if (tooltip) {
return ( return (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger> <TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{tooltip}</p> <p>{tooltip}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
); );
} }
return button; return button;
}; };
export type ArtifactContentProps = HTMLAttributes<HTMLDivElement>; export type ArtifactContentProps = HTMLAttributes<HTMLDivElement>;
export const ArtifactContent = forwardRef<HTMLDivElement, ArtifactContentProps>( export const ArtifactContent = forwardRef<HTMLDivElement, ArtifactContentProps>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"min-h-0 flex-1 overflow-auto bg-background p-4", "min-h-0 flex-1 overflow-auto bg-background p-4",
className, className,
)} )}
{...props} {...props}
/> />
), ),
); );
ArtifactContent.displayName = "ArtifactContent"; ArtifactContent.displayName = "ArtifactContent";
@@ -1,22 +1,22 @@
import { Background, ReactFlow, type ReactFlowProps } from "@xyflow/react"; import { Background, ReactFlow, type ReactFlowProps } from "@xyflow/react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import "@xyflow/react/dist/style.css"; import "@xyflow/react/dist/style.css";
type CanvasProps = ReactFlowProps & { type CanvasProps = ReactFlowProps & {
children?: ReactNode; children?: ReactNode;
}; };
export const Canvas = ({ children, ...props }: CanvasProps) => ( export const Canvas = ({ children, ...props }: CanvasProps) => (
<ReactFlow <ReactFlow
deleteKeyCode={["Backspace", "Delete"]} deleteKeyCode={["Backspace", "Delete"]}
fitView fitView
panOnDrag={false} panOnDrag={false}
panOnScroll panOnScroll
selectionOnDrag={true} selectionOnDrag={true}
zoomOnDoubleClick={false} zoomOnDoubleClick={false}
{...props} {...props}
> >
<Background bgColor="var(--sidebar)" /> <Background bgColor="var(--sidebar)" />
{children} {children}
</ReactFlow> </ReactFlow>
); );
@@ -1,239 +1,239 @@
"use client"; "use client";
import { useControllableState } from "@radix-ui/react-use-controllable-state"; import { useControllableState } from "@radix-ui/react-use-controllable-state";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
BrainIcon, BrainIcon,
ChevronDownIcon, ChevronDownIcon,
DotIcon, DotIcon,
type LucideIcon, type LucideIcon,
} from "lucide-react"; } from "lucide-react";
import type { ComponentProps, ReactNode } from "react"; import type { ComponentProps, ReactNode } from "react";
import { import {
createContext, createContext,
isValidElement, isValidElement,
memo, memo,
useContext, useContext,
useMemo, useMemo,
} from "react"; } from "react";
type ChainOfThoughtContextValue = { type ChainOfThoughtContextValue = {
isOpen: boolean; isOpen: boolean;
setIsOpen: (open: boolean) => void; setIsOpen: (open: boolean) => void;
}; };
const ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>( const ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>(
null, null,
); );
const useChainOfThought = () => { const useChainOfThought = () => {
const context = useContext(ChainOfThoughtContext); const context = useContext(ChainOfThoughtContext);
if (!context) { if (!context) {
throw new Error( throw new Error(
"ChainOfThought components must be used within ChainOfThought", "ChainOfThought components must be used within ChainOfThought",
); );
} }
return context; return context;
}; };
export type ChainOfThoughtProps = ComponentProps<"div"> & { export type ChainOfThoughtProps = ComponentProps<"div"> & {
open?: boolean; open?: boolean;
defaultOpen?: boolean; defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
}; };
export const ChainOfThought = memo( export const ChainOfThought = memo(
({ ({
className, className,
open, open,
defaultOpen = false, defaultOpen = false,
onOpenChange, onOpenChange,
children, children,
...props ...props
}: ChainOfThoughtProps) => { }: ChainOfThoughtProps) => {
const [isOpen, setIsOpen] = useControllableState({ const [isOpen, setIsOpen] = useControllableState({
prop: open, prop: open,
defaultProp: defaultOpen, defaultProp: defaultOpen,
onChange: onOpenChange, onChange: onOpenChange,
}); });
const chainOfThoughtContext = useMemo( const chainOfThoughtContext = useMemo(
() => ({ isOpen, setIsOpen }), () => ({ isOpen, setIsOpen }),
[isOpen, setIsOpen], [isOpen, setIsOpen],
); );
return ( return (
<ChainOfThoughtContext.Provider value={chainOfThoughtContext}> <ChainOfThoughtContext.Provider value={chainOfThoughtContext}>
<div className={cn("not-prose", className)} {...props}> <div className={cn("not-prose", className)} {...props}>
{children} {children}
</div> </div>
</ChainOfThoughtContext.Provider> </ChainOfThoughtContext.Provider>
); );
}, },
); );
export type ChainOfThoughtHeaderProps = ComponentProps< export type ChainOfThoughtHeaderProps = ComponentProps<
typeof CollapsibleTrigger typeof CollapsibleTrigger
> & { > & {
icon?: React.ReactElement; icon?: React.ReactElement;
}; };
export const ChainOfThoughtHeader = memo( export const ChainOfThoughtHeader = memo(
({ className, children, icon, ...props }: ChainOfThoughtHeaderProps) => { ({ className, children, icon, ...props }: ChainOfThoughtHeaderProps) => {
const { isOpen, setIsOpen } = useChainOfThought(); const { isOpen, setIsOpen } = useChainOfThought();
return ( return (
<Collapsible onOpenChange={setIsOpen} open={isOpen}> <Collapsible onOpenChange={setIsOpen} open={isOpen}>
<CollapsibleTrigger <CollapsibleTrigger
className={cn( className={cn(
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors", "text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
className, className,
)} )}
{...props} {...props}
> >
{icon ?? <BrainIcon className="size-4" />} {icon ?? <BrainIcon className="size-4" />}
<span className="flex-1 text-left"> <span className="flex-1 text-left">
{children ?? "Chain of Thought"} {children ?? "Chain of Thought"}
</span> </span>
<ChevronDownIcon <ChevronDownIcon
className={cn( className={cn(
"size-4 transition-transform", "size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0", isOpen ? "rotate-180" : "rotate-0",
)} )}
/> />
</CollapsibleTrigger> </CollapsibleTrigger>
</Collapsible> </Collapsible>
); );
}, },
); );
export type ChainOfThoughtStepProps = ComponentProps<"div"> & { export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
icon?: LucideIcon | React.ReactElement; icon?: LucideIcon | React.ReactElement;
label: ReactNode; label: ReactNode;
description?: ReactNode; description?: ReactNode;
status?: "complete" | "active" | "pending"; status?: "complete" | "active" | "pending";
}; };
export const ChainOfThoughtStep = memo( export const ChainOfThoughtStep = memo(
({ ({
className, className,
icon: Icon = DotIcon, icon: Icon = DotIcon,
label, label,
description, description,
status = "complete", status = "complete",
children, children,
...props ...props
}: ChainOfThoughtStepProps) => { }: ChainOfThoughtStepProps) => {
const statusStyles = { const statusStyles = {
complete: "text-muted-foreground", complete: "text-muted-foreground",
active: "text-foreground", active: "text-foreground",
pending: "text-muted-foreground/50", pending: "text-muted-foreground/50",
}; };
return ( return (
<div <div
className={cn( className={cn(
"flex gap-2 text-sm", "flex gap-2 text-sm",
statusStyles[status], statusStyles[status],
"fade-in-0 slide-in-from-top-2 animate-in", "fade-in-0 slide-in-from-top-2 animate-in",
className, className,
)} )}
{...props} {...props}
> >
<div className="relative mt-0.5"> <div className="relative mt-0.5">
{isValidElement(Icon) ? Icon : <Icon className="size-4" />} {isValidElement(Icon) ? Icon : <Icon className="size-4" />}
<div className="bg-border absolute top-7 bottom-0 left-1/2 -mx-px w-px" /> <div className="bg-border absolute top-7 bottom-0 left-1/2 -mx-px w-px" />
</div> </div>
<div className="flex-1 space-y-2 overflow-hidden"> <div className="flex-1 space-y-2 overflow-hidden">
<div>{label}</div> <div>{label}</div>
{description && ( {description && (
<div className="text-muted-foreground text-xs">{description}</div> <div className="text-muted-foreground text-xs">{description}</div>
)} )}
{children} {children}
</div> </div>
</div> </div>
); );
}, },
); );
export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">; export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">;
export const ChainOfThoughtSearchResults = memo( export const ChainOfThoughtSearchResults = memo(
({ className, ...props }: ChainOfThoughtSearchResultsProps) => ( ({ className, ...props }: ChainOfThoughtSearchResultsProps) => (
<div <div
className={cn( className={cn(
"flex flex-wrap items-center gap-2 overflow-x-hidden", "flex flex-wrap items-center gap-2 overflow-x-hidden",
className, className,
)} )}
{...props} {...props}
/> />
), ),
); );
export type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>; export type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>;
export const ChainOfThoughtSearchResult = memo( export const ChainOfThoughtSearchResult = memo(
({ className, children, ...props }: ChainOfThoughtSearchResultProps) => ( ({ className, children, ...props }: ChainOfThoughtSearchResultProps) => (
<Badge <Badge
className={cn("gap-1 px-2 py-0.5 text-xs font-normal", className)} className={cn("gap-1 px-2 py-0.5 text-xs font-normal", className)}
variant="secondary" variant="secondary"
{...props} {...props}
> >
{children} {children}
</Badge> </Badge>
), ),
); );
export type ChainOfThoughtContentProps = ComponentProps< export type ChainOfThoughtContentProps = ComponentProps<
typeof CollapsibleContent typeof CollapsibleContent
>; >;
export const ChainOfThoughtContent = memo( export const ChainOfThoughtContent = memo(
({ className, children, ...props }: ChainOfThoughtContentProps) => { ({ className, children, ...props }: ChainOfThoughtContentProps) => {
const { isOpen } = useChainOfThought(); const { isOpen } = useChainOfThought();
return ( return (
<Collapsible open={isOpen}> <Collapsible open={isOpen}>
<CollapsibleContent <CollapsibleContent
className={cn( className={cn(
"mt-2 space-y-3", "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", "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, className,
)} )}
{...props} {...props}
> >
{children} {children}
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
); );
}, },
); );
export type ChainOfThoughtImageProps = ComponentProps<"div"> & { export type ChainOfThoughtImageProps = ComponentProps<"div"> & {
caption?: string; caption?: string;
}; };
export const ChainOfThoughtImage = memo( export const ChainOfThoughtImage = memo(
({ className, children, caption, ...props }: ChainOfThoughtImageProps) => ( ({ className, children, caption, ...props }: ChainOfThoughtImageProps) => (
<div className={cn("mt-2 space-y-2", className)} {...props}> <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"> <div className="bg-muted relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg p-3">
{children} {children}
</div> </div>
{caption && <p className="text-muted-foreground text-xs">{caption}</p>} {caption && <p className="text-muted-foreground text-xs">{caption}</p>}
</div> </div>
), ),
); );
ChainOfThought.displayName = "ChainOfThought"; ChainOfThought.displayName = "ChainOfThought";
ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader"; ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader";
ChainOfThoughtStep.displayName = "ChainOfThoughtStep"; ChainOfThoughtStep.displayName = "ChainOfThoughtStep";
ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults"; ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults";
ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult"; ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult";
ChainOfThoughtContent.displayName = "ChainOfThoughtContent"; ChainOfThoughtContent.displayName = "ChainOfThoughtContent";
ChainOfThoughtImage.displayName = "ChainOfThoughtImage"; ChainOfThoughtImage.displayName = "ChainOfThoughtImage";
@@ -1,71 +1,71 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { BookmarkIcon, type LucideProps } from "lucide-react"; import { BookmarkIcon, type LucideProps } from "lucide-react";
import type { ComponentProps, HTMLAttributes } from "react"; import type { ComponentProps, HTMLAttributes } from "react";
export type CheckpointProps = HTMLAttributes<HTMLDivElement>; export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
export const Checkpoint = ({ export const Checkpoint = ({
className, className,
children, children,
...props ...props
}: CheckpointProps) => ( }: CheckpointProps) => (
<div <div
className={cn( className={cn(
"text-muted-foreground flex items-center gap-0.5 overflow-hidden", "text-muted-foreground flex items-center gap-0.5 overflow-hidden",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<Separator /> <Separator />
</div> </div>
); );
export type CheckpointIconProps = LucideProps; export type CheckpointIconProps = LucideProps;
export const CheckpointIcon = ({ export const CheckpointIcon = ({
className, className,
children, children,
...props ...props
}: CheckpointIconProps) => }: CheckpointIconProps) =>
children ?? ( children ?? (
<BookmarkIcon className={cn("size-4 shrink-0", className)} {...props} /> <BookmarkIcon className={cn("size-4 shrink-0", className)} {...props} />
); );
export type CheckpointTriggerProps = ComponentProps<typeof Button> & { export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
tooltip?: string; tooltip?: string;
}; };
export const CheckpointTrigger = ({ export const CheckpointTrigger = ({
children, children,
className, className,
variant = "ghost", variant = "ghost",
size = "sm", size = "sm",
tooltip, tooltip,
...props ...props
}: CheckpointTriggerProps) => }: CheckpointTriggerProps) =>
tooltip ? ( tooltip ? (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button size={size} type="button" variant={variant} {...props}> <Button size={size} type="button" variant={variant} {...props}>
{children} {children}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent align="start" side="bottom"> <TooltipContent align="start" side="bottom">
{tooltip} {tooltip}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
) : ( ) : (
<Button size={size} type="button" variant={variant} {...props}> <Button size={size} type="button" variant={variant} {...props}>
{children} {children}
</Button> </Button>
); );
@@ -1,150 +1,150 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { copyTextToClipboard } from "@/core/clipboard"; import { copyTextToClipboard } from "@/core/clipboard";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CheckIcon, CopyIcon } from "lucide-react"; import { CheckIcon, CopyIcon } from "lucide-react";
import { import {
type ComponentProps, type ComponentProps,
createContext, createContext,
type HTMLAttributes, type HTMLAttributes,
useContext, useContext,
useEffect, useEffect,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki"; import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki";
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
code: string; code: string;
language: BundledLanguage; language: BundledLanguage;
showLineNumbers?: boolean; showLineNumbers?: boolean;
}; };
type CodeBlockContextType = { type CodeBlockContextType = {
code: string; code: string;
}; };
const CodeBlockContext = createContext<CodeBlockContextType>({ const CodeBlockContext = createContext<CodeBlockContextType>({
code: "", code: "",
}); });
const lineNumberTransformer: ShikiTransformer = { const lineNumberTransformer: ShikiTransformer = {
name: "line-numbers", name: "line-numbers",
line(node, line) { line(node, line) {
node.children.unshift({ node.children.unshift({
type: "element", type: "element",
tagName: "span", tagName: "span",
properties: { properties: {
className: [ className: [
"inline-block", "inline-block",
"min-w-10", "min-w-10",
"mr-4", "mr-4",
"text-right", "text-right",
"select-none", "select-none",
"text-muted-foreground", "text-muted-foreground",
], ],
}, },
children: [{ type: "text", value: String(line) }], children: [{ type: "text", value: String(line) }],
}); });
}, },
}; };
export async function highlightCode( export async function highlightCode(
code: string, code: string,
language: BundledLanguage, language: BundledLanguage,
showLineNumbers = false, showLineNumbers = false,
) { ) {
const transformers: ShikiTransformer[] = showLineNumbers const transformers: ShikiTransformer[] = showLineNumbers
? [lineNumberTransformer] ? [lineNumberTransformer]
: []; : [];
return await Promise.all([ return await Promise.all([
codeToHtml(code, { codeToHtml(code, {
lang: language, lang: language,
theme: "one-light", theme: "one-light",
transformers, transformers,
}), }),
codeToHtml(code, { codeToHtml(code, {
lang: language, lang: language,
theme: "one-dark-pro", theme: "one-dark-pro",
transformers, transformers,
}), }),
]); ]);
} }
export const CodeBlock = ({ export const CodeBlock = ({
code, code,
language, language,
showLineNumbers = false, showLineNumbers = false,
className, className,
children, children,
...props ...props
}: CodeBlockProps) => { }: CodeBlockProps) => {
const [html, setHtml] = useState<string>(""); const [html, setHtml] = useState<string>("");
const [darkHtml, setDarkHtml] = useState<string>(""); const [darkHtml, setDarkHtml] = useState<string>("");
const mounted = useRef(false); const mounted = useRef(false);
useEffect(() => { useEffect(() => {
highlightCode(code, language, showLineNumbers).then(([light, dark]) => { highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
if (!mounted.current) { if (!mounted.current) {
setHtml(light); setHtml(light);
setDarkHtml(dark); setDarkHtml(dark);
mounted.current = true; mounted.current = true;
} }
}); });
return () => { return () => {
mounted.current = false; mounted.current = false;
}; };
}, [code, language, showLineNumbers]); }, [code, language, showLineNumbers]);
return ( return (
<CodeBlockContext.Provider value={{ code }}> <CodeBlockContext.Provider value={{ code }}>
<div <div
className={cn( className={cn(
"group bg-background text-foreground relative size-full overflow-hidden rounded-md border", "group bg-background text-foreground relative size-full overflow-hidden rounded-md border",
className, className,
)} )}
{...props} {...props}
> >
<div className="relative size-full"> <div className="relative size-full">
<div <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" 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." // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
dangerouslySetInnerHTML={{ __html: html }} dangerouslySetInnerHTML={{ __html: html }}
/> />
<div <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" 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." // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
dangerouslySetInnerHTML={{ __html: darkHtml }} dangerouslySetInnerHTML={{ __html: darkHtml }}
/> />
{children && ( {children && (
<div className="absolute top-2 right-2 flex items-center gap-2"> <div className="absolute top-2 right-2 flex items-center gap-2">
{children} {children}
</div> </div>
)} )}
</div> </div>
</div> </div>
</CodeBlockContext.Provider> </CodeBlockContext.Provider>
); );
}; };
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
onCopy?: () => void; onCopy?: () => void;
onError?: (error: Error) => void; onError?: (error: Error) => void;
timeout?: number; timeout?: number;
}; };
export const CodeBlockCopyButton = ({ export const CodeBlockCopyButton = ({
onCopy, onCopy,
onError, onError,
timeout = 2000, timeout = 2000,
children, children,
className, className,
...props ...props
}: CodeBlockCopyButtonProps) => { }: CodeBlockCopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
const { code } = useContext(CodeBlockContext); const { code } = useContext(CodeBlockContext);
const copyToClipboard = async () => { const copyToClipboard = async () => {
@@ -152,23 +152,23 @@ export const CodeBlockCopyButton = ({
await copyTextToClipboard(code); await copyTextToClipboard(code);
setIsCopied(true); setIsCopied(true);
onCopy?.(); onCopy?.();
setTimeout(() => setIsCopied(false), timeout); setTimeout(() => setIsCopied(false), timeout);
} catch (error) { } catch (error) {
onError?.(error as Error); onError?.(error as Error);
} }
}; };
const Icon = isCopied ? CheckIcon : CopyIcon; const Icon = isCopied ? CheckIcon : CopyIcon;
return ( return (
<Button <Button
className={cn("shrink-0", className)} className={cn("shrink-0", className)}
onClick={copyToClipboard} onClick={copyToClipboard}
size="icon" size="icon"
variant="ghost" variant="ghost"
{...props} {...props}
> >
{children ?? <Icon size={14} />} {children ?? <Icon size={14} />}
</Button> </Button>
); );
}; };
@@ -1,28 +1,28 @@
import type { ConnectionLineComponent } from "@xyflow/react"; import type { ConnectionLineComponent } from "@xyflow/react";
const HALF = 0.5; const HALF = 0.5;
export const Connection: ConnectionLineComponent = ({ export const Connection: ConnectionLineComponent = ({
fromX, fromX,
fromY, fromY,
toX, toX,
toY, toY,
}) => ( }) => (
<g> <g>
<path <path
className="animated" className="animated"
d={`M${fromX},${fromY} C ${fromX + (toX - fromX) * HALF},${fromY} ${fromX + (toX - fromX) * HALF},${toY} ${toX},${toY}`} d={`M${fromX},${fromY} C ${fromX + (toX - fromX) * HALF},${fromY} ${fromX + (toX - fromX) * HALF},${toY} ${toX},${toY}`}
fill="none" fill="none"
stroke="var(--color-ring)" stroke="var(--color-ring)"
strokeWidth={1} strokeWidth={1}
/> />
<circle <circle
cx={toX} cx={toX}
cy={toY} cy={toY}
fill="#fff" fill="#fff"
r={3} r={3}
stroke="var(--color-ring)" stroke="var(--color-ring)"
strokeWidth={1} strokeWidth={1}
/> />
</g> </g>
); );
@@ -1,408 +1,408 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
HoverCard, HoverCard,
HoverCardContent, HoverCardContent,
HoverCardTrigger, HoverCardTrigger,
} from "@/components/ui/hover-card"; } from "@/components/ui/hover-card";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { LanguageModelUsage } from "ai"; import type { LanguageModelUsage } from "ai";
import { type ComponentProps, createContext, useContext } from "react"; import { type ComponentProps, createContext, useContext } from "react";
import { getUsage } from "tokenlens"; import { getUsage } from "tokenlens";
const PERCENT_MAX = 100; const PERCENT_MAX = 100;
const ICON_RADIUS = 10; const ICON_RADIUS = 10;
const ICON_VIEWBOX = 24; const ICON_VIEWBOX = 24;
const ICON_CENTER = 12; const ICON_CENTER = 12;
const ICON_STROKE_WIDTH = 2; const ICON_STROKE_WIDTH = 2;
type ModelId = string; type ModelId = string;
type ContextSchema = { type ContextSchema = {
usedTokens: number; usedTokens: number;
maxTokens: number; maxTokens: number;
usage?: LanguageModelUsage; usage?: LanguageModelUsage;
modelId?: ModelId; modelId?: ModelId;
}; };
const ContextContext = createContext<ContextSchema | null>(null); const ContextContext = createContext<ContextSchema | null>(null);
const useContextValue = () => { const useContextValue = () => {
const context = useContext(ContextContext); const context = useContext(ContextContext);
if (!context) { if (!context) {
throw new Error("Context components must be used within Context"); throw new Error("Context components must be used within Context");
} }
return context; return context;
}; };
export type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema; export type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema;
export const Context = ({ export const Context = ({
usedTokens, usedTokens,
maxTokens, maxTokens,
usage, usage,
modelId, modelId,
...props ...props
}: ContextProps) => ( }: ContextProps) => (
<ContextContext.Provider <ContextContext.Provider
value={{ value={{
usedTokens, usedTokens,
maxTokens, maxTokens,
usage, usage,
modelId, modelId,
}} }}
> >
<HoverCard closeDelay={0} openDelay={0} {...props} /> <HoverCard closeDelay={0} openDelay={0} {...props} />
</ContextContext.Provider> </ContextContext.Provider>
); );
const ContextIcon = () => { const ContextIcon = () => {
const { usedTokens, maxTokens } = useContextValue(); const { usedTokens, maxTokens } = useContextValue();
const circumference = 2 * Math.PI * ICON_RADIUS; const circumference = 2 * Math.PI * ICON_RADIUS;
const usedPercent = usedTokens / maxTokens; const usedPercent = usedTokens / maxTokens;
const dashOffset = circumference * (1 - usedPercent); const dashOffset = circumference * (1 - usedPercent);
return ( return (
<svg <svg
aria-label="Model context usage" aria-label="Model context usage"
height="20" height="20"
role="img" role="img"
style={{ color: "currentcolor" }} style={{ color: "currentcolor" }}
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`} viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
width="20" width="20"
> >
<circle <circle
cx={ICON_CENTER} cx={ICON_CENTER}
cy={ICON_CENTER} cy={ICON_CENTER}
fill="none" fill="none"
opacity="0.25" opacity="0.25"
r={ICON_RADIUS} r={ICON_RADIUS}
stroke="currentColor" stroke="currentColor"
strokeWidth={ICON_STROKE_WIDTH} strokeWidth={ICON_STROKE_WIDTH}
/> />
<circle <circle
cx={ICON_CENTER} cx={ICON_CENTER}
cy={ICON_CENTER} cy={ICON_CENTER}
fill="none" fill="none"
opacity="0.7" opacity="0.7"
r={ICON_RADIUS} r={ICON_RADIUS}
stroke="currentColor" stroke="currentColor"
strokeDasharray={`${circumference} ${circumference}`} strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={dashOffset} strokeDashoffset={dashOffset}
strokeLinecap="round" strokeLinecap="round"
strokeWidth={ICON_STROKE_WIDTH} strokeWidth={ICON_STROKE_WIDTH}
style={{ transformOrigin: "center", transform: "rotate(-90deg)" }} style={{ transformOrigin: "center", transform: "rotate(-90deg)" }}
/> />
</svg> </svg>
); );
}; };
export type ContextTriggerProps = ComponentProps<typeof Button>; export type ContextTriggerProps = ComponentProps<typeof Button>;
export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => { export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
const { usedTokens, maxTokens } = useContextValue(); const { usedTokens, maxTokens } = useContextValue();
const usedPercent = usedTokens / maxTokens; const usedPercent = usedTokens / maxTokens;
const renderedPercent = new Intl.NumberFormat("en-US", { const renderedPercent = new Intl.NumberFormat("en-US", {
style: "percent", style: "percent",
maximumFractionDigits: 1, maximumFractionDigits: 1,
}).format(usedPercent); }).format(usedPercent);
return ( return (
<HoverCardTrigger asChild> <HoverCardTrigger asChild>
{children ?? ( {children ?? (
<Button type="button" variant="ghost" {...props}> <Button type="button" variant="ghost" {...props}>
<span className="text-muted-foreground font-medium"> <span className="text-muted-foreground font-medium">
{renderedPercent} {renderedPercent}
</span> </span>
<ContextIcon /> <ContextIcon />
</Button> </Button>
)} )}
</HoverCardTrigger> </HoverCardTrigger>
); );
}; };
export type ContextContentProps = ComponentProps<typeof HoverCardContent>; export type ContextContentProps = ComponentProps<typeof HoverCardContent>;
export const ContextContent = ({ export const ContextContent = ({
className, className,
...props ...props
}: ContextContentProps) => ( }: ContextContentProps) => (
<HoverCardContent <HoverCardContent
className={cn("min-w-60 divide-y overflow-hidden p-0", className)} className={cn("min-w-60 divide-y overflow-hidden p-0", className)}
{...props} {...props}
/> />
); );
export type ContextContentHeaderProps = ComponentProps<"div">; export type ContextContentHeaderProps = ComponentProps<"div">;
export const ContextContentHeader = ({ export const ContextContentHeader = ({
children, children,
className, className,
...props ...props
}: ContextContentHeaderProps) => { }: ContextContentHeaderProps) => {
const { usedTokens, maxTokens } = useContextValue(); const { usedTokens, maxTokens } = useContextValue();
const usedPercent = usedTokens / maxTokens; const usedPercent = usedTokens / maxTokens;
const displayPct = new Intl.NumberFormat("en-US", { const displayPct = new Intl.NumberFormat("en-US", {
style: "percent", style: "percent",
maximumFractionDigits: 1, maximumFractionDigits: 1,
}).format(usedPercent); }).format(usedPercent);
const used = new Intl.NumberFormat("en-US", { const used = new Intl.NumberFormat("en-US", {
notation: "compact", notation: "compact",
}).format(usedTokens); }).format(usedTokens);
const total = new Intl.NumberFormat("en-US", { const total = new Intl.NumberFormat("en-US", {
notation: "compact", notation: "compact",
}).format(maxTokens); }).format(maxTokens);
return ( return (
<div className={cn("w-full space-y-2 p-3", className)} {...props}> <div className={cn("w-full space-y-2 p-3", className)} {...props}>
{children ?? ( {children ?? (
<> <>
<div className="flex items-center justify-between gap-3 text-xs"> <div className="flex items-center justify-between gap-3 text-xs">
<p>{displayPct}</p> <p>{displayPct}</p>
<p className="text-muted-foreground font-mono"> <p className="text-muted-foreground font-mono">
{used} / {total} {used} / {total}
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Progress className="bg-muted" value={usedPercent * PERCENT_MAX} /> <Progress className="bg-muted" value={usedPercent * PERCENT_MAX} />
</div> </div>
</> </>
)} )}
</div> </div>
); );
}; };
export type ContextContentBodyProps = ComponentProps<"div">; export type ContextContentBodyProps = ComponentProps<"div">;
export const ContextContentBody = ({ export const ContextContentBody = ({
children, children,
className, className,
...props ...props
}: ContextContentBodyProps) => ( }: ContextContentBodyProps) => (
<div className={cn("w-full p-3", className)} {...props}> <div className={cn("w-full p-3", className)} {...props}>
{children} {children}
</div> </div>
); );
export type ContextContentFooterProps = ComponentProps<"div">; export type ContextContentFooterProps = ComponentProps<"div">;
export const ContextContentFooter = ({ export const ContextContentFooter = ({
children, children,
className, className,
...props ...props
}: ContextContentFooterProps) => { }: ContextContentFooterProps) => {
const { modelId, usage } = useContextValue(); const { modelId, usage } = useContextValue();
const costUSD = modelId const costUSD = modelId
? getUsage({ ? getUsage({
modelId, modelId,
usage: { usage: {
input: usage?.inputTokens ?? 0, input: usage?.inputTokens ?? 0,
output: usage?.outputTokens ?? 0, output: usage?.outputTokens ?? 0,
}, },
}).costUSD?.totalUSD }).costUSD?.totalUSD
: undefined; : undefined;
const totalCost = new Intl.NumberFormat("en-US", { const totalCost = new Intl.NumberFormat("en-US", {
style: "currency", style: "currency",
currency: "USD", currency: "USD",
}).format(costUSD ?? 0); }).format(costUSD ?? 0);
return ( return (
<div <div
className={cn( className={cn(
"bg-secondary flex w-full items-center justify-between gap-3 p-3 text-xs", "bg-secondary flex w-full items-center justify-between gap-3 p-3 text-xs",
className, className,
)} )}
{...props} {...props}
> >
{children ?? ( {children ?? (
<> <>
<span className="text-muted-foreground">Total cost</span> <span className="text-muted-foreground">Total cost</span>
<span>{totalCost}</span> <span>{totalCost}</span>
</> </>
)} )}
</div> </div>
); );
}; };
export type ContextInputUsageProps = ComponentProps<"div">; export type ContextInputUsageProps = ComponentProps<"div">;
export const ContextInputUsage = ({ export const ContextInputUsage = ({
className, className,
children, children,
...props ...props
}: ContextInputUsageProps) => { }: ContextInputUsageProps) => {
const { usage, modelId } = useContextValue(); const { usage, modelId } = useContextValue();
const inputTokens = usage?.inputTokens ?? 0; const inputTokens = usage?.inputTokens ?? 0;
if (children) { if (children) {
return children; return children;
} }
if (!inputTokens) { if (!inputTokens) {
return null; return null;
} }
const inputCost = modelId const inputCost = modelId
? getUsage({ ? getUsage({
modelId, modelId,
usage: { input: inputTokens, output: 0 }, usage: { input: inputTokens, output: 0 },
}).costUSD?.totalUSD }).costUSD?.totalUSD
: undefined; : undefined;
const inputCostText = new Intl.NumberFormat("en-US", { const inputCostText = new Intl.NumberFormat("en-US", {
style: "currency", style: "currency",
currency: "USD", currency: "USD",
}).format(inputCost ?? 0); }).format(inputCost ?? 0);
return ( return (
<div <div
className={cn("flex items-center justify-between text-xs", className)} className={cn("flex items-center justify-between text-xs", className)}
{...props} {...props}
> >
<span className="text-muted-foreground">Input</span> <span className="text-muted-foreground">Input</span>
<TokensWithCost costText={inputCostText} tokens={inputTokens} /> <TokensWithCost costText={inputCostText} tokens={inputTokens} />
</div> </div>
); );
}; };
export type ContextOutputUsageProps = ComponentProps<"div">; export type ContextOutputUsageProps = ComponentProps<"div">;
export const ContextOutputUsage = ({ export const ContextOutputUsage = ({
className, className,
children, children,
...props ...props
}: ContextOutputUsageProps) => { }: ContextOutputUsageProps) => {
const { usage, modelId } = useContextValue(); const { usage, modelId } = useContextValue();
const outputTokens = usage?.outputTokens ?? 0; const outputTokens = usage?.outputTokens ?? 0;
if (children) { if (children) {
return children; return children;
} }
if (!outputTokens) { if (!outputTokens) {
return null; return null;
} }
const outputCost = modelId const outputCost = modelId
? getUsage({ ? getUsage({
modelId, modelId,
usage: { input: 0, output: outputTokens }, usage: { input: 0, output: outputTokens },
}).costUSD?.totalUSD }).costUSD?.totalUSD
: undefined; : undefined;
const outputCostText = new Intl.NumberFormat("en-US", { const outputCostText = new Intl.NumberFormat("en-US", {
style: "currency", style: "currency",
currency: "USD", currency: "USD",
}).format(outputCost ?? 0); }).format(outputCost ?? 0);
return ( return (
<div <div
className={cn("flex items-center justify-between text-xs", className)} className={cn("flex items-center justify-between text-xs", className)}
{...props} {...props}
> >
<span className="text-muted-foreground">Output</span> <span className="text-muted-foreground">Output</span>
<TokensWithCost costText={outputCostText} tokens={outputTokens} /> <TokensWithCost costText={outputCostText} tokens={outputTokens} />
</div> </div>
); );
}; };
export type ContextReasoningUsageProps = ComponentProps<"div">; export type ContextReasoningUsageProps = ComponentProps<"div">;
export const ContextReasoningUsage = ({ export const ContextReasoningUsage = ({
className, className,
children, children,
...props ...props
}: ContextReasoningUsageProps) => { }: ContextReasoningUsageProps) => {
const { usage, modelId } = useContextValue(); const { usage, modelId } = useContextValue();
const reasoningTokens = usage?.reasoningTokens ?? 0; const reasoningTokens = usage?.reasoningTokens ?? 0;
if (children) { if (children) {
return children; return children;
} }
if (!reasoningTokens) { if (!reasoningTokens) {
return null; return null;
} }
const reasoningCost = modelId const reasoningCost = modelId
? getUsage({ ? getUsage({
modelId, modelId,
usage: { reasoningTokens }, usage: { reasoningTokens },
}).costUSD?.totalUSD }).costUSD?.totalUSD
: undefined; : undefined;
const reasoningCostText = new Intl.NumberFormat("en-US", { const reasoningCostText = new Intl.NumberFormat("en-US", {
style: "currency", style: "currency",
currency: "USD", currency: "USD",
}).format(reasoningCost ?? 0); }).format(reasoningCost ?? 0);
return ( return (
<div <div
className={cn("flex items-center justify-between text-xs", className)} className={cn("flex items-center justify-between text-xs", className)}
{...props} {...props}
> >
<span className="text-muted-foreground">Reasoning</span> <span className="text-muted-foreground">Reasoning</span>
<TokensWithCost costText={reasoningCostText} tokens={reasoningTokens} /> <TokensWithCost costText={reasoningCostText} tokens={reasoningTokens} />
</div> </div>
); );
}; };
export type ContextCacheUsageProps = ComponentProps<"div">; export type ContextCacheUsageProps = ComponentProps<"div">;
export const ContextCacheUsage = ({ export const ContextCacheUsage = ({
className, className,
children, children,
...props ...props
}: ContextCacheUsageProps) => { }: ContextCacheUsageProps) => {
const { usage, modelId } = useContextValue(); const { usage, modelId } = useContextValue();
const cacheTokens = usage?.cachedInputTokens ?? 0; const cacheTokens = usage?.cachedInputTokens ?? 0;
if (children) { if (children) {
return children; return children;
} }
if (!cacheTokens) { if (!cacheTokens) {
return null; return null;
} }
const cacheCost = modelId const cacheCost = modelId
? getUsage({ ? getUsage({
modelId, modelId,
usage: { cacheReads: cacheTokens, input: 0, output: 0 }, usage: { cacheReads: cacheTokens, input: 0, output: 0 },
}).costUSD?.totalUSD }).costUSD?.totalUSD
: undefined; : undefined;
const cacheCostText = new Intl.NumberFormat("en-US", { const cacheCostText = new Intl.NumberFormat("en-US", {
style: "currency", style: "currency",
currency: "USD", currency: "USD",
}).format(cacheCost ?? 0); }).format(cacheCost ?? 0);
return ( return (
<div <div
className={cn("flex items-center justify-between text-xs", className)} className={cn("flex items-center justify-between text-xs", className)}
{...props} {...props}
> >
<span className="text-muted-foreground">Cache</span> <span className="text-muted-foreground">Cache</span>
<TokensWithCost costText={cacheCostText} tokens={cacheTokens} /> <TokensWithCost costText={cacheCostText} tokens={cacheTokens} />
</div> </div>
); );
}; };
const TokensWithCost = ({ const TokensWithCost = ({
tokens, tokens,
costText, costText,
}: { }: {
tokens?: number; tokens?: number;
costText?: string; costText?: string;
}) => ( }) => (
<span> <span>
{tokens === undefined {tokens === undefined
? "—" ? "—"
: new Intl.NumberFormat("en-US", { : new Intl.NumberFormat("en-US", {
notation: "compact", notation: "compact",
}).format(tokens)} }).format(tokens)}
{costText ? ( {costText ? (
<span className="text-muted-foreground ml-2">• {costText}</span> <span className="text-muted-foreground ml-2">• {costText}</span>
) : null} ) : null}
</span> </span>
); );
@@ -1,18 +1,18 @@
"use client"; "use client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Controls as ControlsPrimitive } from "@xyflow/react"; import { Controls as ControlsPrimitive } from "@xyflow/react";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
export type ControlsProps = ComponentProps<typeof ControlsPrimitive>; export type ControlsProps = ComponentProps<typeof ControlsPrimitive>;
export const Controls = ({ className, ...props }: ControlsProps) => ( export const Controls = ({ className, ...props }: ControlsProps) => (
<ControlsPrimitive <ControlsPrimitive
className={cn( className={cn(
"bg-card gap-px overflow-hidden rounded-md border p-1 shadow-none!", "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!", "[&>button]:hover:bg-secondary! [&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent!",
className, className,
)} )}
{...props} {...props}
/> />
); );
@@ -1,100 +1,100 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ArrowDownIcon } from "lucide-react"; import { ArrowDownIcon } from "lucide-react";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { useCallback } from "react"; import { useCallback } from "react";
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
export type ConversationProps = ComponentProps<typeof StickToBottom>; export type ConversationProps = ComponentProps<typeof StickToBottom>;
export const Conversation = ({ className, ...props }: ConversationProps) => ( export const Conversation = ({ className, ...props }: ConversationProps) => (
<StickToBottom <StickToBottom
className={cn("relative flex-1 overflow-y-hidden", className)} className={cn("relative flex-1 overflow-y-hidden", className)}
initial="smooth" initial="smooth"
resize="smooth" resize="smooth"
role="log" role="log"
{...props} {...props}
/> />
); );
export type ConversationContentProps = ComponentProps< export type ConversationContentProps = ComponentProps<
typeof StickToBottom.Content typeof StickToBottom.Content
>; >;
export const ConversationContent = ({ export const ConversationContent = ({
className, className,
...props ...props
}: ConversationContentProps) => ( }: ConversationContentProps) => (
<StickToBottom.Content <StickToBottom.Content
className={cn("flex flex-col gap-8 p-4", className)} className={cn("flex flex-col gap-8 p-4", className)}
{...props} {...props}
/> />
); );
export type ConversationEmptyStateProps = ComponentProps<"div"> & { export type ConversationEmptyStateProps = ComponentProps<"div"> & {
title?: string; title?: string;
description?: string; description?: string;
icon?: React.ReactNode; icon?: React.ReactNode;
}; };
export const ConversationEmptyState = ({ export const ConversationEmptyState = ({
className, className,
title = "No messages yet", title = "No messages yet",
description = "Start a conversation to see messages here", description = "Start a conversation to see messages here",
icon, icon,
children, children,
...props ...props
}: ConversationEmptyStateProps) => ( }: ConversationEmptyStateProps) => (
<div <div
className={cn( className={cn(
"flex size-full flex-col items-center justify-center gap-3 p-8 text-center", "flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
className, className,
)} )}
{...props} {...props}
> >
{children ?? ( {children ?? (
<> <>
{icon && <div className="text-muted-foreground">{icon}</div>} {icon && <div className="text-muted-foreground">{icon}</div>}
<div className="space-y-1"> <div className="space-y-1">
<h3 className="text-sm font-medium">{title}</h3> <h3 className="text-sm font-medium">{title}</h3>
{description && ( {description && (
<p className="text-muted-foreground text-sm">{description}</p> <p className="text-muted-foreground text-sm">{description}</p>
)} )}
</div> </div>
</> </>
)} )}
</div> </div>
); );
export type ConversationScrollButtonProps = ComponentProps<typeof Button>; export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
export const ConversationScrollButton = ({ export const ConversationScrollButton = ({
className, className,
...props ...props
}: ConversationScrollButtonProps) => { }: ConversationScrollButtonProps) => {
const { isAtBottom, scrollToBottom } = useStickToBottomContext(); const { isAtBottom, scrollToBottom } = useStickToBottomContext();
const handleScrollToBottom = useCallback(() => { const handleScrollToBottom = useCallback(() => {
scrollToBottom(); scrollToBottom();
}, [scrollToBottom]); }, [scrollToBottom]);
return ( return (
!isAtBottom && ( !isAtBottom && (
<Button <Button
className={cn( className={cn(
"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full", "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full",
className, className,
)} )}
onClick={handleScrollToBottom} onClick={handleScrollToBottom}
size="icon" size="icon"
type="button" type="button"
variant="outline" variant="outline"
{...props} {...props}
> >
<ArrowDownIcon className="size-4" /> <ArrowDownIcon className="size-4" />
</Button> </Button>
) )
); );
}; };
+140 -140
View File
@@ -1,140 +1,140 @@
import { import {
BaseEdge, BaseEdge,
type EdgeProps, type EdgeProps,
getBezierPath, getBezierPath,
getSimpleBezierPath, getSimpleBezierPath,
type InternalNode, type InternalNode,
type Node, type Node,
Position, Position,
useInternalNode, useInternalNode,
} from "@xyflow/react"; } from "@xyflow/react";
const Temporary = ({ const Temporary = ({
id, id,
sourceX, sourceX,
sourceY, sourceY,
targetX, targetX,
targetY, targetY,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
}: EdgeProps) => { }: EdgeProps) => {
const [edgePath] = getSimpleBezierPath({ const [edgePath] = getSimpleBezierPath({
sourceX, sourceX,
sourceY, sourceY,
sourcePosition, sourcePosition,
targetX, targetX,
targetY, targetY,
targetPosition, targetPosition,
}); });
return ( return (
<BaseEdge <BaseEdge
className="stroke-ring stroke-1" className="stroke-ring stroke-1"
id={id} id={id}
path={edgePath} path={edgePath}
style={{ style={{
strokeDasharray: "5, 5", strokeDasharray: "5, 5",
}} }}
/> />
); );
}; };
const getHandleCoordsByPosition = ( const getHandleCoordsByPosition = (
node: InternalNode<Node>, node: InternalNode<Node>,
handlePosition: Position, handlePosition: Position,
) => { ) => {
// Choose the handle type based on position - Left is for target, Right is for source // Choose the handle type based on position - Left is for target, Right is for source
const handleType = handlePosition === Position.Left ? "target" : "source"; const handleType = handlePosition === Position.Left ? "target" : "source";
const handle = node.internals.handleBounds?.[handleType]?.find( const handle = node.internals.handleBounds?.[handleType]?.find(
(h) => h.position === handlePosition, (h) => h.position === handlePosition,
); );
if (!handle) { if (!handle) {
return [0, 0] as const; return [0, 0] as const;
} }
let offsetX = handle.width / 2; let offsetX = handle.width / 2;
let offsetY = handle.height / 2; let offsetY = handle.height / 2;
// this is a tiny detail to make the markerEnd of an edge visible. // 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 // 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 // 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) { switch (handlePosition) {
case Position.Left: case Position.Left:
offsetX = 0; offsetX = 0;
break; break;
case Position.Right: case Position.Right:
offsetX = handle.width; offsetX = handle.width;
break; break;
case Position.Top: case Position.Top:
offsetY = 0; offsetY = 0;
break; break;
case Position.Bottom: case Position.Bottom:
offsetY = handle.height; offsetY = handle.height;
break; break;
default: default:
throw new Error(`Invalid handle position: ${handlePosition}`); throw new Error(`Invalid handle position: ${handlePosition}`);
} }
const x = node.internals.positionAbsolute.x + handle.x + offsetX; const x = node.internals.positionAbsolute.x + handle.x + offsetX;
const y = node.internals.positionAbsolute.y + handle.y + offsetY; const y = node.internals.positionAbsolute.y + handle.y + offsetY;
return [x, y] as const; return [x, y] as const;
}; };
const getEdgeParams = ( const getEdgeParams = (
source: InternalNode<Node>, source: InternalNode<Node>,
target: InternalNode<Node>, target: InternalNode<Node>,
) => { ) => {
const sourcePos = Position.Right; const sourcePos = Position.Right;
const [sx, sy] = getHandleCoordsByPosition(source, sourcePos); const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
const targetPos = Position.Left; const targetPos = Position.Left;
const [tx, ty] = getHandleCoordsByPosition(target, targetPos); const [tx, ty] = getHandleCoordsByPosition(target, targetPos);
return { return {
sx, sx,
sy, sy,
tx, tx,
ty, ty,
sourcePos, sourcePos,
targetPos, targetPos,
}; };
}; };
const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => { const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
const sourceNode = useInternalNode(source); const sourceNode = useInternalNode(source);
const targetNode = useInternalNode(target); const targetNode = useInternalNode(target);
if (!(sourceNode && targetNode)) { if (!(sourceNode && targetNode)) {
return null; return null;
} }
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams( const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
sourceNode, sourceNode,
targetNode, targetNode,
); );
const [edgePath] = getBezierPath({ const [edgePath] = getBezierPath({
sourceX: sx, sourceX: sx,
sourceY: sy, sourceY: sy,
sourcePosition: sourcePos, sourcePosition: sourcePos,
targetX: tx, targetX: tx,
targetY: ty, targetY: ty,
targetPosition: targetPos, targetPosition: targetPos,
}); });
return ( return (
<> <>
<BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} /> <BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} />
<circle fill="var(--primary)" r="4"> <circle fill="var(--primary)" r="4">
<animateMotion dur="2s" path={edgePath} repeatCount="indefinite" /> <animateMotion dur="2s" path={edgePath} repeatCount="indefinite" />
</circle> </circle>
</> </>
); );
}; };
export const Edge = { export const Edge = {
Temporary, Temporary,
Animated, Animated,
}; };
@@ -1,24 +1,24 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Experimental_GeneratedImage } from "ai"; import type { Experimental_GeneratedImage } from "ai";
export type ImageProps = Experimental_GeneratedImage & { export type ImageProps = Experimental_GeneratedImage & {
className?: string; className?: string;
alt?: string; alt?: string;
}; };
export const Image = ({ export const Image = ({
base64, base64,
uint8Array, uint8Array,
mediaType, mediaType,
...props ...props
}: ImageProps) => ( }: ImageProps) => (
<img <img
{...props} {...props}
alt={props.alt} alt={props.alt}
className={cn( className={cn(
"h-auto max-w-full overflow-hidden rounded-md", "h-auto max-w-full overflow-hidden rounded-md",
props.className, props.className,
)} )}
src={`data:${mediaType};base64,${base64}`} src={`data:${mediaType};base64,${base64}`}
/> />
); );
@@ -1,96 +1,96 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "react"; import type { HTMLAttributes } from "react";
type LoaderIconProps = { type LoaderIconProps = {
size?: number; size?: number;
}; };
const LoaderIcon = ({ size = 16 }: LoaderIconProps) => ( const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
<svg <svg
height={size} height={size}
strokeLinejoin="round" strokeLinejoin="round"
style={{ color: "currentcolor" }} style={{ color: "currentcolor" }}
viewBox="0 0 16 16" viewBox="0 0 16 16"
width={size} width={size}
> >
<title>Loader</title> <title>Loader</title>
<g clipPath="url(#clip0_2393_1490)"> <g clipPath="url(#clip0_2393_1490)">
<path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" /> <path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" />
<path <path
d="M8 16V12" d="M8 16V12"
opacity="0.5" opacity="0.5"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
/> />
<path <path
d="M3.29773 1.52783L5.64887 4.7639" d="M3.29773 1.52783L5.64887 4.7639"
opacity="0.9" opacity="0.9"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
/> />
<path <path
d="M12.7023 1.52783L10.3511 4.7639" d="M12.7023 1.52783L10.3511 4.7639"
opacity="0.1" opacity="0.1"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
/> />
<path <path
d="M12.7023 14.472L10.3511 11.236" d="M12.7023 14.472L10.3511 11.236"
opacity="0.4" opacity="0.4"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
/> />
<path <path
d="M3.29773 14.472L5.64887 11.236" d="M3.29773 14.472L5.64887 11.236"
opacity="0.6" opacity="0.6"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
/> />
<path <path
d="M15.6085 5.52783L11.8043 6.7639" d="M15.6085 5.52783L11.8043 6.7639"
opacity="0.2" opacity="0.2"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
/> />
<path <path
d="M0.391602 10.472L4.19583 9.23598" d="M0.391602 10.472L4.19583 9.23598"
opacity="0.7" opacity="0.7"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
/> />
<path <path
d="M15.6085 10.4722L11.8043 9.2361" d="M15.6085 10.4722L11.8043 9.2361"
opacity="0.3" opacity="0.3"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
/> />
<path <path
d="M0.391602 5.52783L4.19583 6.7639" d="M0.391602 5.52783L4.19583 6.7639"
opacity="0.8" opacity="0.8"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
/> />
</g> </g>
<defs> <defs>
<clipPath id="clip0_2393_1490"> <clipPath id="clip0_2393_1490">
<rect fill="white" height="16" width="16" /> <rect fill="white" height="16" width="16" />
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
); );
export type LoaderProps = HTMLAttributes<HTMLDivElement> & { export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
size?: number; size?: number;
}; };
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => ( export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
<div <div
className={cn( className={cn(
"inline-flex animate-spin items-center justify-center", "inline-flex animate-spin items-center justify-center",
className, className,
)} )}
{...props} {...props}
> >
<LoaderIcon size={size} /> <LoaderIcon size={size} />
</div> </div>
); );
@@ -1,446 +1,446 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group"; import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { FileUIPart, UIMessage } from "ai"; import type { FileUIPart, UIMessage } from "ai";
import { import {
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
PaperclipIcon, PaperclipIcon,
XIcon, XIcon,
} from "lucide-react"; } from "lucide-react";
import type { ComponentProps, HTMLAttributes, ReactElement } from "react"; import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
import { createContext, memo, useContext, useEffect, useState } from "react"; import { createContext, memo, useContext, useEffect, useState } from "react";
import { Streamdown } from "streamdown"; import { Streamdown } from "streamdown";
export type MessageProps = HTMLAttributes<HTMLDivElement> & { export type MessageProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"]; from: UIMessage["role"];
}; };
export const Message = ({ className, from, ...props }: MessageProps) => ( export const Message = ({ className, from, ...props }: MessageProps) => (
<div <div
className={cn( className={cn(
"group flex w-full flex-col gap-2", "group flex w-full flex-col gap-2",
from === "user" ? "is-user ml-auto justify-end" : "is-assistant", from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
className, className,
)} )}
{...props} {...props}
/> />
); );
export type MessageContentProps = HTMLAttributes<HTMLDivElement>; export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
export const MessageContent = ({ export const MessageContent = ({
children, children,
className, className,
...props ...props
}: MessageContentProps) => ( }: MessageContentProps) => (
<div <div
className={cn( className={cn(
"is-user:dark flex w-fit max-w-full min-w-0 flex-col gap-2 overflow-visible", "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]: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-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", "group-[.is-assistant]:text-foreground",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
</div> </div>
); );
export type MessageActionsProps = ComponentProps<"div">; export type MessageActionsProps = ComponentProps<"div">;
export const MessageActions = ({ export const MessageActions = ({
className, className,
children, children,
...props ...props
}: MessageActionsProps) => ( }: MessageActionsProps) => (
<div className={cn("flex items-center gap-1", className)} {...props}> <div className={cn("flex items-center gap-1", className)} {...props}>
{children} {children}
</div> </div>
); );
export type MessageActionProps = ComponentProps<typeof Button> & { export type MessageActionProps = ComponentProps<typeof Button> & {
tooltip?: string; tooltip?: string;
label?: string; label?: string;
}; };
export const MessageAction = ({ export const MessageAction = ({
tooltip, tooltip,
children, children,
label, label,
variant = "ghost", variant = "ghost",
size = "icon-sm", size = "icon-sm",
...props ...props
}: MessageActionProps) => { }: MessageActionProps) => {
const button = ( const button = (
<Button size={size} type="button" variant={variant} {...props}> <Button size={size} type="button" variant={variant} {...props}>
{children} {children}
<span className="sr-only">{label || tooltip}</span> <span className="sr-only">{label || tooltip}</span>
</Button> </Button>
); );
if (tooltip) { if (tooltip) {
return ( return (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger> <TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{tooltip}</p> <p>{tooltip}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
); );
} }
return button; return button;
}; };
type MessageBranchContextType = { type MessageBranchContextType = {
currentBranch: number; currentBranch: number;
totalBranches: number; totalBranches: number;
goToPrevious: () => void; goToPrevious: () => void;
goToNext: () => void; goToNext: () => void;
branches: ReactElement[]; branches: ReactElement[];
setBranches: (branches: ReactElement[]) => void; setBranches: (branches: ReactElement[]) => void;
}; };
const MessageBranchContext = createContext<MessageBranchContextType | null>( const MessageBranchContext = createContext<MessageBranchContextType | null>(
null, null,
); );
const useMessageBranch = () => { const useMessageBranch = () => {
const context = useContext(MessageBranchContext); const context = useContext(MessageBranchContext);
if (!context) { if (!context) {
throw new Error( throw new Error(
"MessageBranch components must be used within MessageBranch", "MessageBranch components must be used within MessageBranch",
); );
} }
return context; return context;
}; };
export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & { export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
defaultBranch?: number; defaultBranch?: number;
onBranchChange?: (branchIndex: number) => void; onBranchChange?: (branchIndex: number) => void;
}; };
export const MessageBranch = ({ export const MessageBranch = ({
defaultBranch = 0, defaultBranch = 0,
onBranchChange, onBranchChange,
className, className,
...props ...props
}: MessageBranchProps) => { }: MessageBranchProps) => {
const [currentBranch, setCurrentBranch] = useState(defaultBranch); const [currentBranch, setCurrentBranch] = useState(defaultBranch);
const [branches, setBranches] = useState<ReactElement[]>([]); const [branches, setBranches] = useState<ReactElement[]>([]);
const handleBranchChange = (newBranch: number) => { const handleBranchChange = (newBranch: number) => {
setCurrentBranch(newBranch); setCurrentBranch(newBranch);
onBranchChange?.(newBranch); onBranchChange?.(newBranch);
}; };
const goToPrevious = () => { const goToPrevious = () => {
const newBranch = const newBranch =
currentBranch > 0 ? currentBranch - 1 : branches.length - 1; currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
handleBranchChange(newBranch); handleBranchChange(newBranch);
}; };
const goToNext = () => { const goToNext = () => {
const newBranch = const newBranch =
currentBranch < branches.length - 1 ? currentBranch + 1 : 0; currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
handleBranchChange(newBranch); handleBranchChange(newBranch);
}; };
const contextValue: MessageBranchContextType = { const contextValue: MessageBranchContextType = {
currentBranch, currentBranch,
totalBranches: branches.length, totalBranches: branches.length,
goToPrevious, goToPrevious,
goToNext, goToNext,
branches, branches,
setBranches, setBranches,
}; };
return ( return (
<MessageBranchContext.Provider value={contextValue}> <MessageBranchContext.Provider value={contextValue}>
<div <div
className={cn("grid w-full gap-2 [&>div]:pb-0", className)} className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
{...props} {...props}
/> />
</MessageBranchContext.Provider> </MessageBranchContext.Provider>
); );
}; };
export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>; export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
export const MessageBranchContent = ({ export const MessageBranchContent = ({
children, children,
...props ...props
}: MessageBranchContentProps) => { }: MessageBranchContentProps) => {
const { currentBranch, setBranches, branches } = useMessageBranch(); const { currentBranch, setBranches, branches } = useMessageBranch();
const childrenArray = Array.isArray(children) ? children : [children]; const childrenArray = Array.isArray(children) ? children : [children];
// Use useEffect to update branches when they change // Use useEffect to update branches when they change
useEffect(() => { useEffect(() => {
if (branches.length !== childrenArray.length) { if (branches.length !== childrenArray.length) {
setBranches(childrenArray); setBranches(childrenArray);
} }
}, [childrenArray, branches, setBranches]); }, [childrenArray, branches, setBranches]);
return childrenArray.map((branch, index) => ( return childrenArray.map((branch, index) => (
<div <div
className={cn( className={cn(
"grid gap-2 overflow-hidden [&>div]:pb-0", "grid gap-2 overflow-hidden [&>div]:pb-0",
index === currentBranch ? "block" : "hidden", index === currentBranch ? "block" : "hidden",
)} )}
key={branch.key} key={branch.key}
{...props} {...props}
> >
{branch} {branch}
</div> </div>
)); ));
}; };
export type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & { export type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"]; from: UIMessage["role"];
}; };
export const MessageBranchSelector = ({ export const MessageBranchSelector = ({
className, className,
from, from,
...props ...props
}: MessageBranchSelectorProps) => { }: MessageBranchSelectorProps) => {
const { totalBranches } = useMessageBranch(); const { totalBranches } = useMessageBranch();
// Don't render if there's only one branch // Don't render if there's only one branch
if (totalBranches <= 1) { if (totalBranches <= 1) {
return null; return null;
} }
return ( return (
<ButtonGroup <ButtonGroup
className="[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md" className="[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md"
orientation="horizontal" orientation="horizontal"
{...props} {...props}
/> />
); );
}; };
export type MessageBranchPreviousProps = ComponentProps<typeof Button>; export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
export const MessageBranchPrevious = ({ export const MessageBranchPrevious = ({
children, children,
...props ...props
}: MessageBranchPreviousProps) => { }: MessageBranchPreviousProps) => {
const { goToPrevious, totalBranches } = useMessageBranch(); const { goToPrevious, totalBranches } = useMessageBranch();
return ( return (
<Button <Button
aria-label="Previous branch" aria-label="Previous branch"
disabled={totalBranches <= 1} disabled={totalBranches <= 1}
onClick={goToPrevious} onClick={goToPrevious}
size="icon-sm" size="icon-sm"
type="button" type="button"
variant="ghost" variant="ghost"
{...props} {...props}
> >
{children ?? <ChevronLeftIcon size={14} />} {children ?? <ChevronLeftIcon size={14} />}
</Button> </Button>
); );
}; };
export type MessageBranchNextProps = ComponentProps<typeof Button>; export type MessageBranchNextProps = ComponentProps<typeof Button>;
export const MessageBranchNext = ({ export const MessageBranchNext = ({
children, children,
className, className,
...props ...props
}: MessageBranchNextProps) => { }: MessageBranchNextProps) => {
const { goToNext, totalBranches } = useMessageBranch(); const { goToNext, totalBranches } = useMessageBranch();
return ( return (
<Button <Button
aria-label="Next branch" aria-label="Next branch"
disabled={totalBranches <= 1} disabled={totalBranches <= 1}
onClick={goToNext} onClick={goToNext}
size="icon-sm" size="icon-sm"
type="button" type="button"
variant="ghost" variant="ghost"
{...props} {...props}
> >
{children ?? <ChevronRightIcon size={14} />} {children ?? <ChevronRightIcon size={14} />}
</Button> </Button>
); );
}; };
export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>; export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
export const MessageBranchPage = ({ export const MessageBranchPage = ({
className, className,
...props ...props
}: MessageBranchPageProps) => { }: MessageBranchPageProps) => {
const { currentBranch, totalBranches } = useMessageBranch(); const { currentBranch, totalBranches } = useMessageBranch();
return ( return (
<ButtonGroupText <ButtonGroupText
className={cn( className={cn(
"text-muted-foreground border-none bg-transparent shadow-none", "text-muted-foreground border-none bg-transparent shadow-none",
className, className,
)} )}
{...props} {...props}
> >
{currentBranch + 1} of {totalBranches} {currentBranch + 1} of {totalBranches}
</ButtonGroupText> </ButtonGroupText>
); );
}; };
export type MessageResponseProps = ComponentProps<typeof Streamdown>; export type MessageResponseProps = ComponentProps<typeof Streamdown>;
export const MessageResponse = memo( export const MessageResponse = memo(
({ className, ...props }: MessageResponseProps) => ( ({ className, ...props }: MessageResponseProps) => (
<Streamdown <Streamdown
className={cn( className={cn(
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0", "size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
className, className,
)} )}
{...props} {...props}
/> />
), ),
(prevProps, nextProps) => prevProps.children === nextProps.children, (prevProps, nextProps) => prevProps.children === nextProps.children,
); );
MessageResponse.displayName = "MessageResponse"; MessageResponse.displayName = "MessageResponse";
export type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & { export type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
data: FileUIPart; data: FileUIPart;
className?: string; className?: string;
onRemove?: () => void; onRemove?: () => void;
}; };
export function MessageAttachment({ export function MessageAttachment({
data, data,
className, className,
onRemove, onRemove,
...props ...props
}: MessageAttachmentProps) { }: MessageAttachmentProps) {
const filename = data.filename || ""; const filename = data.filename || "";
const mediaType = const mediaType =
data.mediaType?.startsWith("image/") && data.url ? "image" : "file"; data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
const isImage = mediaType === "image"; const isImage = mediaType === "image";
const attachmentLabel = filename || (isImage ? "Image" : "Attachment"); const attachmentLabel = filename || (isImage ? "Image" : "Attachment");
return ( return (
<div <div
className={cn( className={cn(
"group relative size-24 overflow-hidden rounded-lg", "group relative size-24 overflow-hidden rounded-lg",
className, className,
)} )}
{...props} {...props}
> >
{isImage ? ( {isImage ? (
<> <>
<img <img
alt={filename || "attachment"} alt={filename || "attachment"}
className="size-full object-cover" className="size-full object-cover"
height={100} height={100}
src={data.url} src={data.url}
width={100} width={100}
/> />
{onRemove && ( {onRemove && (
<Button <Button
aria-label="Remove attachment" 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" 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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onRemove(); onRemove();
}} }}
type="button" type="button"
variant="ghost" variant="ghost"
> >
<XIcon /> <XIcon />
<span className="sr-only">Remove</span> <span className="sr-only">Remove</span>
</Button> </Button>
)} )}
</> </>
) : ( ) : (
<> <>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="bg-muted text-muted-foreground flex size-full shrink-0 items-center justify-center rounded-lg"> <div className="bg-muted text-muted-foreground flex size-full shrink-0 items-center justify-center rounded-lg">
<PaperclipIcon className="size-4" /> <PaperclipIcon className="size-4" />
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{attachmentLabel}</p> <p>{attachmentLabel}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
{onRemove && ( {onRemove && (
<Button <Button
aria-label="Remove attachment" 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" 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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onRemove(); onRemove();
}} }}
type="button" type="button"
variant="ghost" variant="ghost"
> >
<XIcon /> <XIcon />
<span className="sr-only">Remove</span> <span className="sr-only">Remove</span>
</Button> </Button>
)} )}
</> </>
)} )}
</div> </div>
); );
} }
export type MessageAttachmentsProps = ComponentProps<"div">; export type MessageAttachmentsProps = ComponentProps<"div">;
export function MessageAttachments({ export function MessageAttachments({
children, children,
className, className,
...props ...props
}: MessageAttachmentsProps) { }: MessageAttachmentsProps) {
if (!children) { if (!children) {
return null; return null;
} }
return ( return (
<div <div
className={cn( className={cn(
"ml-auto flex w-fit flex-wrap items-start gap-2", "ml-auto flex w-fit flex-wrap items-start gap-2",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
</div> </div>
); );
} }
export type MessageToolbarProps = ComponentProps<"div">; export type MessageToolbarProps = ComponentProps<"div">;
export const MessageToolbar = ({ export const MessageToolbar = ({
className, className,
children, children,
...props ...props
}: MessageToolbarProps) => ( }: MessageToolbarProps) => (
<div <div
className={cn( className={cn(
"mt-4 flex w-full items-center justify-between gap-4", "mt-4 flex w-full items-center justify-between gap-4",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
</div> </div>
); );
@@ -1,208 +1,208 @@
import { import {
Command, Command,
CommandDialog, CommandDialog,
CommandEmpty, CommandEmpty,
CommandGroup, CommandGroup,
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
CommandSeparator, CommandSeparator,
CommandShortcut, CommandShortcut,
} from "@/components/ui/command"; } from "@/components/ui/command";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { ComponentProps, ReactNode } from "react"; import type { ComponentProps, ReactNode } from "react";
export type ModelSelectorProps = ComponentProps<typeof Dialog>; export type ModelSelectorProps = ComponentProps<typeof Dialog>;
export const ModelSelector = (props: ModelSelectorProps) => ( export const ModelSelector = (props: ModelSelectorProps) => (
<Dialog {...props} /> <Dialog {...props} />
); );
export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>; export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>;
export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => ( export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
<DialogTrigger {...props} /> <DialogTrigger {...props} />
); );
export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & { export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
title?: ReactNode; title?: ReactNode;
}; };
export const ModelSelectorContent = ({ export const ModelSelectorContent = ({
className, className,
children, children,
title = "Model Selector", title = "Model Selector",
...props ...props
}: ModelSelectorContentProps) => ( }: ModelSelectorContentProps) => (
<DialogContent className={cn("p-0", className)} {...props}> <DialogContent className={cn("p-0", className)} {...props}>
<DialogTitle className="sr-only">{title}</DialogTitle> <DialogTitle className="sr-only">{title}</DialogTitle>
<Command className="**:data-[slot=command-input-wrapper]:h-auto"> <Command className="**:data-[slot=command-input-wrapper]:h-auto">
{children} {children}
</Command> </Command>
</DialogContent> </DialogContent>
); );
export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>; export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>;
export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => ( export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
<CommandDialog {...props} /> <CommandDialog {...props} />
); );
export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>; export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>;
export const ModelSelectorInput = ({ export const ModelSelectorInput = ({
className, className,
...props ...props
}: ModelSelectorInputProps) => ( }: ModelSelectorInputProps) => (
<CommandInput className={cn("h-auto py-3.5", className)} {...props} /> <CommandInput className={cn("h-auto py-3.5", className)} {...props} />
); );
export type ModelSelectorListProps = ComponentProps<typeof CommandList>; export type ModelSelectorListProps = ComponentProps<typeof CommandList>;
export const ModelSelectorList = (props: ModelSelectorListProps) => ( export const ModelSelectorList = (props: ModelSelectorListProps) => (
<CommandList {...props} /> <CommandList {...props} />
); );
export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>; export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>;
export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => ( export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
<CommandEmpty {...props} /> <CommandEmpty {...props} />
); );
export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>; export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>;
export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => ( export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
<CommandGroup {...props} /> <CommandGroup {...props} />
); );
export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>; export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>;
export const ModelSelectorItem = (props: ModelSelectorItemProps) => ( export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
<CommandItem {...props} /> <CommandItem {...props} />
); );
export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>; export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>;
export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => ( export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
<CommandShortcut {...props} /> <CommandShortcut {...props} />
); );
export type ModelSelectorSeparatorProps = ComponentProps< export type ModelSelectorSeparatorProps = ComponentProps<
typeof CommandSeparator typeof CommandSeparator
>; >;
export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => ( export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
<CommandSeparator {...props} /> <CommandSeparator {...props} />
); );
export type ModelSelectorLogoProps = Omit< export type ModelSelectorLogoProps = Omit<
ComponentProps<"img">, ComponentProps<"img">,
"src" | "alt" "src" | "alt"
> & { > & {
provider: provider:
| "moonshotai-cn" | "moonshotai-cn"
| "lucidquery" | "lucidquery"
| "moonshotai" | "moonshotai"
| "zai-coding-plan" | "zai-coding-plan"
| "alibaba" | "alibaba"
| "xai" | "xai"
| "vultr" | "vultr"
| "nvidia" | "nvidia"
| "upstage" | "upstage"
| "groq" | "groq"
| "github-copilot" | "github-copilot"
| "mistral" | "mistral"
| "vercel" | "vercel"
| "nebius" | "nebius"
| "deepseek" | "deepseek"
| "alibaba-cn" | "alibaba-cn"
| "google-vertex-anthropic" | "google-vertex-anthropic"
| "venice" | "venice"
| "chutes" | "chutes"
| "cortecs" | "cortecs"
| "github-models" | "github-models"
| "togetherai" | "togetherai"
| "azure" | "azure"
| "baseten" | "baseten"
| "huggingface" | "huggingface"
| "opencode" | "opencode"
| "fastrouter" | "fastrouter"
| "google" | "google"
| "google-vertex" | "google-vertex"
| "cloudflare-workers-ai" | "cloudflare-workers-ai"
| "inception" | "inception"
| "wandb" | "wandb"
| "openai" | "openai"
| "zhipuai-coding-plan" | "zhipuai-coding-plan"
| "perplexity" | "perplexity"
| "openrouter" | "openrouter"
| "zenmux" | "zenmux"
| "v0" | "v0"
| "iflowcn" | "iflowcn"
| "synthetic" | "synthetic"
| "deepinfra" | "deepinfra"
| "zhipuai" | "zhipuai"
| "submodel" | "submodel"
| "zai" | "zai"
| "inference" | "inference"
| "requesty" | "requesty"
| "morph" | "morph"
| "lmstudio" | "lmstudio"
| "anthropic" | "anthropic"
| "aihubmix" | "aihubmix"
| "fireworks-ai" | "fireworks-ai"
| "modelscope" | "modelscope"
| "llama" | "llama"
| "scaleway" | "scaleway"
| "amazon-bedrock" | "amazon-bedrock"
| "cerebras" | "cerebras"
| (string & {}); | (string & {});
}; };
export const ModelSelectorLogo = ({ export const ModelSelectorLogo = ({
provider, provider,
className, className,
...props ...props
}: ModelSelectorLogoProps) => ( }: ModelSelectorLogoProps) => (
<img <img
{...props} {...props}
alt={`${provider} logo`} alt={`${provider} logo`}
className={cn("size-3 dark:invert", className)} className={cn("size-3 dark:invert", className)}
height={12} height={12}
src={`https://models.dev/logos/${provider}.svg`} src={`https://models.dev/logos/${provider}.svg`}
width={12} width={12}
/> />
); );
export type ModelSelectorLogoGroupProps = ComponentProps<"div">; export type ModelSelectorLogoGroupProps = ComponentProps<"div">;
export const ModelSelectorLogoGroup = ({ export const ModelSelectorLogoGroup = ({
className, className,
...props ...props
}: ModelSelectorLogoGroupProps) => ( }: ModelSelectorLogoGroupProps) => (
<div <div
className={cn( 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", "[&>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, className,
)} )}
{...props} {...props}
/> />
); );
export type ModelSelectorNameProps = ComponentProps<"span">; export type ModelSelectorNameProps = ComponentProps<"span">;
export const ModelSelectorName = ({ export const ModelSelectorName = ({
className, className,
...props ...props
}: ModelSelectorNameProps) => ( }: ModelSelectorNameProps) => (
<span <span
className={cn("flex-1 truncate text-left text-xs", className)} className={cn("flex-1 truncate text-left text-xs", className)}
{...props} {...props}
/> />
); );
@@ -1,71 +1,71 @@
import { import {
Card, Card,
CardAction, CardAction,
CardContent, CardContent,
CardDescription, CardDescription,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Handle, Position } from "@xyflow/react"; import { Handle, Position } from "@xyflow/react";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
export type NodeProps = ComponentProps<typeof Card> & { export type NodeProps = ComponentProps<typeof Card> & {
handles: { handles: {
target: boolean; target: boolean;
source: boolean; source: boolean;
}; };
}; };
export const Node = ({ handles, className, ...props }: NodeProps) => ( export const Node = ({ handles, className, ...props }: NodeProps) => (
<Card <Card
className={cn( className={cn(
"node-container relative size-full h-auto w-sm gap-0 rounded-md p-0", "node-container relative size-full h-auto w-sm gap-0 rounded-md p-0",
className, className,
)} )}
{...props} {...props}
> >
{handles.target && <Handle position={Position.Left} type="target" />} {handles.target && <Handle position={Position.Left} type="target" />}
{handles.source && <Handle position={Position.Right} type="source" />} {handles.source && <Handle position={Position.Right} type="source" />}
{props.children} {props.children}
</Card> </Card>
); );
export type NodeHeaderProps = ComponentProps<typeof CardHeader>; export type NodeHeaderProps = ComponentProps<typeof CardHeader>;
export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => ( export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
<CardHeader <CardHeader
className={cn("bg-secondary gap-0.5 rounded-t-md border-b p-3!", className)} className={cn("bg-secondary gap-0.5 rounded-t-md border-b p-3!", className)}
{...props} {...props}
/> />
); );
export type NodeTitleProps = ComponentProps<typeof CardTitle>; export type NodeTitleProps = ComponentProps<typeof CardTitle>;
export const NodeTitle = (props: NodeTitleProps) => <CardTitle {...props} />; export const NodeTitle = (props: NodeTitleProps) => <CardTitle {...props} />;
export type NodeDescriptionProps = ComponentProps<typeof CardDescription>; export type NodeDescriptionProps = ComponentProps<typeof CardDescription>;
export const NodeDescription = (props: NodeDescriptionProps) => ( export const NodeDescription = (props: NodeDescriptionProps) => (
<CardDescription {...props} /> <CardDescription {...props} />
); );
export type NodeActionProps = ComponentProps<typeof CardAction>; export type NodeActionProps = ComponentProps<typeof CardAction>;
export const NodeAction = (props: NodeActionProps) => <CardAction {...props} />; export const NodeAction = (props: NodeActionProps) => <CardAction {...props} />;
export type NodeContentProps = ComponentProps<typeof CardContent>; export type NodeContentProps = ComponentProps<typeof CardContent>;
export const NodeContent = ({ className, ...props }: NodeContentProps) => ( export const NodeContent = ({ className, ...props }: NodeContentProps) => (
<CardContent className={cn("p-3", className)} {...props} /> <CardContent className={cn("p-3", className)} {...props} />
); );
export type NodeFooterProps = ComponentProps<typeof CardFooter>; export type NodeFooterProps = ComponentProps<typeof CardFooter>;
export const NodeFooter = ({ className, ...props }: NodeFooterProps) => ( export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
<CardFooter <CardFooter
className={cn("bg-secondary rounded-b-md border-t p-3!", className)} className={cn("bg-secondary rounded-b-md border-t p-3!", className)}
{...props} {...props}
/> />
); );
@@ -1,365 +1,365 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
ChevronDownIcon, ChevronDownIcon,
ExternalLinkIcon, ExternalLinkIcon,
MessageCircleIcon, MessageCircleIcon,
} from "lucide-react"; } from "lucide-react";
import { type ComponentProps, createContext, useContext } from "react"; import { type ComponentProps, createContext, useContext } from "react";
const providers = { const providers = {
github: { github: {
title: "Open in GitHub", title: "Open in GitHub",
createUrl: (url: string) => url, createUrl: (url: string) => url,
icon: ( icon: (
<svg fill="currentColor" role="img" viewBox="0 0 24 24"> <svg fill="currentColor" role="img" viewBox="0 0 24 24">
<title>GitHub</title> <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" /> <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> </svg>
), ),
}, },
scira: { scira: {
title: "Open in Scira", title: "Open in Scira",
createUrl: (q: string) => createUrl: (q: string) =>
`https://scira.ai/?${new URLSearchParams({ `https://scira.ai/?${new URLSearchParams({
q, q,
})}`, })}`,
icon: ( icon: (
<svg <svg
fill="none" fill="none"
height="934" height="934"
viewBox="0 0 910 934" viewBox="0 0 910 934"
width="910" width="910"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<title>Scira AI</title> <title>Scira AI</title>
<path <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" 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" fill="currentColor"
stroke="currentColor" stroke="currentColor"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="8" strokeWidth="8"
/> />
<path <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" 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" fill="currentColor"
stroke="currentColor" stroke="currentColor"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="8" strokeWidth="8"
/> />
<path <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" 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" stroke="currentColor"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="20" strokeWidth="20"
/> />
<path <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" 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" stroke="currentColor"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="20" strokeWidth="20"
/> />
<path <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" 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" fill="currentColor"
stroke="currentColor" stroke="currentColor"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="8" strokeWidth="8"
/> />
<path <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" 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" fill="currentColor"
stroke="currentColor" stroke="currentColor"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="8" strokeWidth="8"
/> />
<path <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" 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" stroke="currentColor"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="30" strokeWidth="30"
/> />
</svg> </svg>
), ),
}, },
chatgpt: { chatgpt: {
title: "Open in ChatGPT", title: "Open in ChatGPT",
createUrl: (prompt: string) => createUrl: (prompt: string) =>
`https://chatgpt.com/?${new URLSearchParams({ `https://chatgpt.com/?${new URLSearchParams({
hints: "search", hints: "search",
prompt, prompt,
})}`, })}`,
icon: ( icon: (
<svg <svg
fill="currentColor" fill="currentColor"
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<title>OpenAI</title> <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" /> <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> </svg>
), ),
}, },
claude: { claude: {
title: "Open in Claude", title: "Open in Claude",
createUrl: (q: string) => createUrl: (q: string) =>
`https://claude.ai/new?${new URLSearchParams({ `https://claude.ai/new?${new URLSearchParams({
q, q,
})}`, })}`,
icon: ( icon: (
<svg <svg
fill="currentColor" fill="currentColor"
role="img" role="img"
viewBox="0 0 12 12" viewBox="0 0 12 12"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<title>Claude</title> <title>Claude</title>
<path <path
clipRule="evenodd" 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" 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" fillRule="evenodd"
/> />
</svg> </svg>
), ),
}, },
t3: { t3: {
title: "Open in T3 Chat", title: "Open in T3 Chat",
createUrl: (q: string) => createUrl: (q: string) =>
`https://t3.chat/new?${new URLSearchParams({ `https://t3.chat/new?${new URLSearchParams({
q, q,
})}`, })}`,
icon: <MessageCircleIcon />, icon: <MessageCircleIcon />,
}, },
v0: { v0: {
title: "Open in v0", title: "Open in v0",
createUrl: (q: string) => createUrl: (q: string) =>
`https://v0.app?${new URLSearchParams({ `https://v0.app?${new URLSearchParams({
q, q,
})}`, })}`,
icon: ( icon: (
<svg <svg
fill="currentColor" fill="currentColor"
viewBox="0 0 147 70" viewBox="0 0 147 70"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<title>v0</title> <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="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" /> <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> </svg>
), ),
}, },
cursor: { cursor: {
title: "Open in Cursor", title: "Open in Cursor",
createUrl: (text: string) => { createUrl: (text: string) => {
const url = new URL("https://cursor.com/link/prompt"); const url = new URL("https://cursor.com/link/prompt");
url.searchParams.set("text", text); url.searchParams.set("text", text);
return url.toString(); return url.toString();
}, },
icon: ( icon: (
<svg <svg
version="1.1" version="1.1"
viewBox="0 0 466.73 532.09" viewBox="0 0 466.73 532.09"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<title>Cursor</title> <title>Cursor</title>
<path <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" 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" fill="currentColor"
/> />
</svg> </svg>
), ),
}, },
}; };
const OpenInContext = createContext<{ query: string } | undefined>(undefined); const OpenInContext = createContext<{ query: string } | undefined>(undefined);
const useOpenInContext = () => { const useOpenInContext = () => {
const context = useContext(OpenInContext); const context = useContext(OpenInContext);
if (!context) { if (!context) {
throw new Error("OpenIn components must be used within an OpenIn provider"); throw new Error("OpenIn components must be used within an OpenIn provider");
} }
return context; return context;
}; };
export type OpenInProps = ComponentProps<typeof DropdownMenu> & { export type OpenInProps = ComponentProps<typeof DropdownMenu> & {
query: string; query: string;
}; };
export const OpenIn = ({ query, ...props }: OpenInProps) => ( export const OpenIn = ({ query, ...props }: OpenInProps) => (
<OpenInContext.Provider value={{ query }}> <OpenInContext.Provider value={{ query }}>
<DropdownMenu {...props} /> <DropdownMenu {...props} />
</OpenInContext.Provider> </OpenInContext.Provider>
); );
export type OpenInContentProps = ComponentProps<typeof DropdownMenuContent>; export type OpenInContentProps = ComponentProps<typeof DropdownMenuContent>;
export const OpenInContent = ({ className, ...props }: OpenInContentProps) => ( export const OpenInContent = ({ className, ...props }: OpenInContentProps) => (
<DropdownMenuContent <DropdownMenuContent
align="start" align="start"
className={cn("w-[240px]", className)} className={cn("w-[240px]", className)}
{...props} {...props}
/> />
); );
export type OpenInItemProps = ComponentProps<typeof DropdownMenuItem>; export type OpenInItemProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInItem = (props: OpenInItemProps) => ( export const OpenInItem = (props: OpenInItemProps) => (
<DropdownMenuItem {...props} /> <DropdownMenuItem {...props} />
); );
export type OpenInLabelProps = ComponentProps<typeof DropdownMenuLabel>; export type OpenInLabelProps = ComponentProps<typeof DropdownMenuLabel>;
export const OpenInLabel = (props: OpenInLabelProps) => ( export const OpenInLabel = (props: OpenInLabelProps) => (
<DropdownMenuLabel {...props} /> <DropdownMenuLabel {...props} />
); );
export type OpenInSeparatorProps = ComponentProps<typeof DropdownMenuSeparator>; export type OpenInSeparatorProps = ComponentProps<typeof DropdownMenuSeparator>;
export const OpenInSeparator = (props: OpenInSeparatorProps) => ( export const OpenInSeparator = (props: OpenInSeparatorProps) => (
<DropdownMenuSeparator {...props} /> <DropdownMenuSeparator {...props} />
); );
export type OpenInTriggerProps = ComponentProps<typeof DropdownMenuTrigger>; export type OpenInTriggerProps = ComponentProps<typeof DropdownMenuTrigger>;
export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => ( export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => (
<DropdownMenuTrigger {...props} asChild> <DropdownMenuTrigger {...props} asChild>
{children ?? ( {children ?? (
<Button type="button" variant="outline"> <Button type="button" variant="outline">
Open in chat Open in chat
<ChevronDownIcon className="size-4" /> <ChevronDownIcon className="size-4" />
</Button> </Button>
)} )}
</DropdownMenuTrigger> </DropdownMenuTrigger>
); );
export type OpenInChatGPTProps = ComponentProps<typeof DropdownMenuItem>; export type OpenInChatGPTProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInChatGPT = (props: OpenInChatGPTProps) => { export const OpenInChatGPT = (props: OpenInChatGPTProps) => {
const { query } = useOpenInContext(); const { query } = useOpenInContext();
return ( return (
<DropdownMenuItem asChild {...props}> <DropdownMenuItem asChild {...props}>
<a <a
className="flex items-center gap-2" className="flex items-center gap-2"
href={providers.chatgpt.createUrl(query)} href={providers.chatgpt.createUrl(query)}
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
<span className="shrink-0">{providers.chatgpt.icon}</span> <span className="shrink-0">{providers.chatgpt.icon}</span>
<span className="flex-1">{providers.chatgpt.title}</span> <span className="flex-1">{providers.chatgpt.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" /> <ExternalLinkIcon className="size-4 shrink-0" />
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
); );
}; };
export type OpenInClaudeProps = ComponentProps<typeof DropdownMenuItem>; export type OpenInClaudeProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInClaude = (props: OpenInClaudeProps) => { export const OpenInClaude = (props: OpenInClaudeProps) => {
const { query } = useOpenInContext(); const { query } = useOpenInContext();
return ( return (
<DropdownMenuItem asChild {...props}> <DropdownMenuItem asChild {...props}>
<a <a
className="flex items-center gap-2" className="flex items-center gap-2"
href={providers.claude.createUrl(query)} href={providers.claude.createUrl(query)}
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
<span className="shrink-0">{providers.claude.icon}</span> <span className="shrink-0">{providers.claude.icon}</span>
<span className="flex-1">{providers.claude.title}</span> <span className="flex-1">{providers.claude.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" /> <ExternalLinkIcon className="size-4 shrink-0" />
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
); );
}; };
export type OpenInT3Props = ComponentProps<typeof DropdownMenuItem>; export type OpenInT3Props = ComponentProps<typeof DropdownMenuItem>;
export const OpenInT3 = (props: OpenInT3Props) => { export const OpenInT3 = (props: OpenInT3Props) => {
const { query } = useOpenInContext(); const { query } = useOpenInContext();
return ( return (
<DropdownMenuItem asChild {...props}> <DropdownMenuItem asChild {...props}>
<a <a
className="flex items-center gap-2" className="flex items-center gap-2"
href={providers.t3.createUrl(query)} href={providers.t3.createUrl(query)}
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
<span className="shrink-0">{providers.t3.icon}</span> <span className="shrink-0">{providers.t3.icon}</span>
<span className="flex-1">{providers.t3.title}</span> <span className="flex-1">{providers.t3.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" /> <ExternalLinkIcon className="size-4 shrink-0" />
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
); );
}; };
export type OpenInSciraProps = ComponentProps<typeof DropdownMenuItem>; export type OpenInSciraProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInScira = (props: OpenInSciraProps) => { export const OpenInScira = (props: OpenInSciraProps) => {
const { query } = useOpenInContext(); const { query } = useOpenInContext();
return ( return (
<DropdownMenuItem asChild {...props}> <DropdownMenuItem asChild {...props}>
<a <a
className="flex items-center gap-2" className="flex items-center gap-2"
href={providers.scira.createUrl(query)} href={providers.scira.createUrl(query)}
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
<span className="shrink-0">{providers.scira.icon}</span> <span className="shrink-0">{providers.scira.icon}</span>
<span className="flex-1">{providers.scira.title}</span> <span className="flex-1">{providers.scira.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" /> <ExternalLinkIcon className="size-4 shrink-0" />
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
); );
}; };
export type OpenInv0Props = ComponentProps<typeof DropdownMenuItem>; export type OpenInv0Props = ComponentProps<typeof DropdownMenuItem>;
export const OpenInv0 = (props: OpenInv0Props) => { export const OpenInv0 = (props: OpenInv0Props) => {
const { query } = useOpenInContext(); const { query } = useOpenInContext();
return ( return (
<DropdownMenuItem asChild {...props}> <DropdownMenuItem asChild {...props}>
<a <a
className="flex items-center gap-2" className="flex items-center gap-2"
href={providers.v0.createUrl(query)} href={providers.v0.createUrl(query)}
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
<span className="shrink-0">{providers.v0.icon}</span> <span className="shrink-0">{providers.v0.icon}</span>
<span className="flex-1">{providers.v0.title}</span> <span className="flex-1">{providers.v0.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" /> <ExternalLinkIcon className="size-4 shrink-0" />
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
); );
}; };
export type OpenInCursorProps = ComponentProps<typeof DropdownMenuItem>; export type OpenInCursorProps = ComponentProps<typeof DropdownMenuItem>;
export const OpenInCursor = (props: OpenInCursorProps) => { export const OpenInCursor = (props: OpenInCursorProps) => {
const { query } = useOpenInContext(); const { query } = useOpenInContext();
return ( return (
<DropdownMenuItem asChild {...props}> <DropdownMenuItem asChild {...props}>
<a <a
className="flex items-center gap-2" className="flex items-center gap-2"
href={providers.cursor.createUrl(query)} href={providers.cursor.createUrl(query)}
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
<span className="shrink-0">{providers.cursor.icon}</span> <span className="shrink-0">{providers.cursor.icon}</span>
<span className="flex-1">{providers.cursor.title}</span> <span className="flex-1">{providers.cursor.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" /> <ExternalLinkIcon className="size-4 shrink-0" />
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
); );
}; };
@@ -1,15 +1,15 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Panel as PanelPrimitive } from "@xyflow/react"; import { Panel as PanelPrimitive } from "@xyflow/react";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
type PanelProps = ComponentProps<typeof PanelPrimitive>; type PanelProps = ComponentProps<typeof PanelPrimitive>;
export const Panel = ({ className, ...props }: PanelProps) => ( export const Panel = ({ className, ...props }: PanelProps) => (
<PanelPrimitive <PanelPrimitive
className={cn( className={cn(
"bg-card m-4 overflow-hidden rounded-md border p-1", "bg-card m-4 overflow-hidden rounded-md border p-1",
className, className,
)} )}
{...props} {...props}
/> />
); );
+142 -142
View File
@@ -1,142 +1,142 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
CardAction, CardAction,
CardContent, CardContent,
CardDescription, CardDescription,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ChevronsUpDownIcon } from "lucide-react"; import { ChevronsUpDownIcon } from "lucide-react";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
import { Shimmer } from "./shimmer"; import { Shimmer } from "./shimmer";
type PlanContextValue = { type PlanContextValue = {
isStreaming: boolean; isStreaming: boolean;
}; };
const PlanContext = createContext<PlanContextValue | null>(null); const PlanContext = createContext<PlanContextValue | null>(null);
const usePlan = () => { const usePlan = () => {
const context = useContext(PlanContext); const context = useContext(PlanContext);
if (!context) { if (!context) {
throw new Error("Plan components must be used within Plan"); throw new Error("Plan components must be used within Plan");
} }
return context; return context;
}; };
export type PlanProps = ComponentProps<typeof Collapsible> & { export type PlanProps = ComponentProps<typeof Collapsible> & {
isStreaming?: boolean; isStreaming?: boolean;
}; };
export const Plan = ({ export const Plan = ({
className, className,
isStreaming = false, isStreaming = false,
children, children,
...props ...props
}: PlanProps) => ( }: PlanProps) => (
<PlanContext.Provider value={{ isStreaming }}> <PlanContext.Provider value={{ isStreaming }}>
<Collapsible asChild data-slot="plan" {...props}> <Collapsible asChild data-slot="plan" {...props}>
<Card className={cn("shadow-none", className)}>{children}</Card> <Card className={cn("shadow-none", className)}>{children}</Card>
</Collapsible> </Collapsible>
</PlanContext.Provider> </PlanContext.Provider>
); );
export type PlanHeaderProps = ComponentProps<typeof CardHeader>; export type PlanHeaderProps = ComponentProps<typeof CardHeader>;
export const PlanHeader = ({ className, ...props }: PlanHeaderProps) => ( export const PlanHeader = ({ className, ...props }: PlanHeaderProps) => (
<CardHeader <CardHeader
className={cn("flex items-start justify-between", className)} className={cn("flex items-start justify-between", className)}
data-slot="plan-header" data-slot="plan-header"
{...props} {...props}
/> />
); );
export type PlanTitleProps = Omit< export type PlanTitleProps = Omit<
ComponentProps<typeof CardTitle>, ComponentProps<typeof CardTitle>,
"children" "children"
> & { > & {
children: string; children: string;
}; };
export const PlanTitle = ({ children, ...props }: PlanTitleProps) => { export const PlanTitle = ({ children, ...props }: PlanTitleProps) => {
const { isStreaming } = usePlan(); const { isStreaming } = usePlan();
return ( return (
<CardTitle data-slot="plan-title" {...props}> <CardTitle data-slot="plan-title" {...props}>
{isStreaming ? <Shimmer>{children}</Shimmer> : children} {isStreaming ? <Shimmer>{children}</Shimmer> : children}
</CardTitle> </CardTitle>
); );
}; };
export type PlanDescriptionProps = Omit< export type PlanDescriptionProps = Omit<
ComponentProps<typeof CardDescription>, ComponentProps<typeof CardDescription>,
"children" "children"
> & { > & {
children: string; children: string;
}; };
export const PlanDescription = ({ export const PlanDescription = ({
className, className,
children, children,
...props ...props
}: PlanDescriptionProps) => { }: PlanDescriptionProps) => {
const { isStreaming } = usePlan(); const { isStreaming } = usePlan();
return ( return (
<CardDescription <CardDescription
className={cn("text-balance", className)} className={cn("text-balance", className)}
data-slot="plan-description" data-slot="plan-description"
{...props} {...props}
> >
{isStreaming ? <Shimmer>{children}</Shimmer> : children} {isStreaming ? <Shimmer>{children}</Shimmer> : children}
</CardDescription> </CardDescription>
); );
}; };
export type PlanActionProps = ComponentProps<typeof CardAction>; export type PlanActionProps = ComponentProps<typeof CardAction>;
export const PlanAction = (props: PlanActionProps) => ( export const PlanAction = (props: PlanActionProps) => (
<CardAction data-slot="plan-action" {...props} /> <CardAction data-slot="plan-action" {...props} />
); );
export type PlanContentProps = ComponentProps<typeof CardContent>; export type PlanContentProps = ComponentProps<typeof CardContent>;
export const PlanContent = (props: PlanContentProps) => ( export const PlanContent = (props: PlanContentProps) => (
<CollapsibleContent asChild> <CollapsibleContent asChild>
<CardContent data-slot="plan-content" {...props} /> <CardContent data-slot="plan-content" {...props} />
</CollapsibleContent> </CollapsibleContent>
); );
export type PlanFooterProps = ComponentProps<"div">; export type PlanFooterProps = ComponentProps<"div">;
export const PlanFooter = (props: PlanFooterProps) => ( export const PlanFooter = (props: PlanFooterProps) => (
<CardFooter data-slot="plan-footer" {...props} /> <CardFooter data-slot="plan-footer" {...props} />
); );
export type PlanTriggerProps = ComponentProps<typeof CollapsibleTrigger>; export type PlanTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => ( export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => (
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button <Button
className={cn("size-8", className)} className={cn("size-8", className)}
data-slot="plan-trigger" data-slot="plan-trigger"
size="icon" size="icon"
variant="ghost" variant="ghost"
{...props} {...props}
> >
<ChevronsUpDownIcon className="size-4" /> <ChevronsUpDownIcon className="size-4" />
<span className="sr-only">Toggle plan</span> <span className="sr-only">Toggle plan</span>
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
); );
File diff suppressed because it is too large. Load diff
+274 -274
View File
@@ -1,274 +1,274 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ChevronDownIcon, PaperclipIcon } from "lucide-react"; import { ChevronDownIcon, PaperclipIcon } from "lucide-react";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
export type QueueMessagePart = { export type QueueMessagePart = {
type: string; type: string;
text?: string; text?: string;
url?: string; url?: string;
filename?: string; filename?: string;
mediaType?: string; mediaType?: string;
}; };
export type QueueMessage = { export type QueueMessage = {
id: string; id: string;
parts: QueueMessagePart[]; parts: QueueMessagePart[];
}; };
export type QueueTodo = { export type QueueTodo = {
id: string; id: string;
title: string; title: string;
description?: string; description?: string;
status?: "pending" | "completed"; status?: "pending" | "completed";
}; };
export type QueueItemProps = ComponentProps<"li">; export type QueueItemProps = ComponentProps<"li">;
export const QueueItem = ({ className, ...props }: QueueItemProps) => ( export const QueueItem = ({ className, ...props }: QueueItemProps) => (
<li <li
className={cn( className={cn(
"group hover:bg-muted flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors", "group hover:bg-muted flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors",
className, className,
)} )}
{...props} {...props}
/> />
); );
export type QueueItemIndicatorProps = ComponentProps<"span"> & { export type QueueItemIndicatorProps = ComponentProps<"span"> & {
completed?: boolean; completed?: boolean;
}; };
export const QueueItemIndicator = ({ export const QueueItemIndicator = ({
completed = false, completed = false,
className, className,
...props ...props
}: QueueItemIndicatorProps) => ( }: QueueItemIndicatorProps) => (
<span <span
className={cn( className={cn(
"mt-0.5 inline-block size-2.5 rounded-full border", "mt-0.5 inline-block size-2.5 rounded-full border",
completed completed
? "border-muted-foreground/20 bg-muted-foreground/10" ? "border-muted-foreground/20 bg-muted-foreground/10"
: "border-muted-foreground/50", : "border-muted-foreground/50",
className, className,
)} )}
{...props} {...props}
/> />
); );
export type QueueItemContentProps = ComponentProps<"span"> & { export type QueueItemContentProps = ComponentProps<"span"> & {
completed?: boolean; completed?: boolean;
}; };
export const QueueItemContent = ({ export const QueueItemContent = ({
completed = false, completed = false,
className, className,
...props ...props
}: QueueItemContentProps) => ( }: QueueItemContentProps) => (
<span <span
className={cn( className={cn(
"line-clamp-1 grow break-words", "line-clamp-1 grow break-words",
completed completed
? "text-muted-foreground/50 line-through" ? "text-muted-foreground/50 line-through"
: "text-muted-foreground", : "text-muted-foreground",
className, className,
)} )}
{...props} {...props}
/> />
); );
export type QueueItemDescriptionProps = ComponentProps<"div"> & { export type QueueItemDescriptionProps = ComponentProps<"div"> & {
completed?: boolean; completed?: boolean;
}; };
export const QueueItemDescription = ({ export const QueueItemDescription = ({
completed = false, completed = false,
className, className,
...props ...props
}: QueueItemDescriptionProps) => ( }: QueueItemDescriptionProps) => (
<div <div
className={cn( className={cn(
"ml-6 text-xs", "ml-6 text-xs",
completed completed
? "text-muted-foreground/40 line-through" ? "text-muted-foreground/40 line-through"
: "text-muted-foreground", : "text-muted-foreground",
className, className,
)} )}
{...props} {...props}
/> />
); );
export type QueueItemActionsProps = ComponentProps<"div">; export type QueueItemActionsProps = ComponentProps<"div">;
export const QueueItemActions = ({ export const QueueItemActions = ({
className, className,
...props ...props
}: QueueItemActionsProps) => ( }: QueueItemActionsProps) => (
<div className={cn("flex gap-1", className)} {...props} /> <div className={cn("flex gap-1", className)} {...props} />
); );
export type QueueItemActionProps = Omit< export type QueueItemActionProps = Omit<
ComponentProps<typeof Button>, ComponentProps<typeof Button>,
"variant" | "size" "variant" | "size"
>; >;
export const QueueItemAction = ({ export const QueueItemAction = ({
className, className,
...props ...props
}: QueueItemActionProps) => ( }: QueueItemActionProps) => (
<Button <Button
className={cn( 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", "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, className,
)} )}
size="icon" size="icon"
type="button" type="button"
variant="ghost" variant="ghost"
{...props} {...props}
/> />
); );
export type QueueItemAttachmentProps = ComponentProps<"div">; export type QueueItemAttachmentProps = ComponentProps<"div">;
export const QueueItemAttachment = ({ export const QueueItemAttachment = ({
className, className,
...props ...props
}: QueueItemAttachmentProps) => ( }: QueueItemAttachmentProps) => (
<div className={cn("mt-1 flex flex-wrap gap-2", className)} {...props} /> <div className={cn("mt-1 flex flex-wrap gap-2", className)} {...props} />
); );
export type QueueItemImageProps = ComponentProps<"img">; export type QueueItemImageProps = ComponentProps<"img">;
export const QueueItemImage = ({ export const QueueItemImage = ({
className, className,
...props ...props
}: QueueItemImageProps) => ( }: QueueItemImageProps) => (
<img <img
alt="" alt=""
className={cn("h-8 w-8 rounded border object-cover", className)} className={cn("h-8 w-8 rounded border object-cover", className)}
height={32} height={32}
width={32} width={32}
{...props} {...props}
/> />
); );
export type QueueItemFileProps = ComponentProps<"span">; export type QueueItemFileProps = ComponentProps<"span">;
export const QueueItemFile = ({ export const QueueItemFile = ({
children, children,
className, className,
...props ...props
}: QueueItemFileProps) => ( }: QueueItemFileProps) => (
<span <span
className={cn( className={cn(
"bg-muted flex items-center gap-1 rounded border px-2 py-1 text-xs", "bg-muted flex items-center gap-1 rounded border px-2 py-1 text-xs",
className, className,
)} )}
{...props} {...props}
> >
<PaperclipIcon size={12} /> <PaperclipIcon size={12} />
<span className="max-w-[100px] truncate">{children}</span> <span className="max-w-[100px] truncate">{children}</span>
</span> </span>
); );
export type QueueListProps = ComponentProps<typeof ScrollArea>; export type QueueListProps = ComponentProps<typeof ScrollArea>;
export const QueueList = ({ export const QueueList = ({
children, children,
className, className,
...props ...props
}: QueueListProps) => ( }: QueueListProps) => (
<ScrollArea className={cn("mt-2 -mb-1", className)} {...props}> <ScrollArea className={cn("mt-2 -mb-1", className)} {...props}>
<div className="max-h-40 pr-4"> <div className="max-h-40 pr-4">
<ul>{children}</ul> <ul>{children}</ul>
</div> </div>
</ScrollArea> </ScrollArea>
); );
// QueueSection - collapsible section container // QueueSection - collapsible section container
export type QueueSectionProps = ComponentProps<typeof Collapsible>; export type QueueSectionProps = ComponentProps<typeof Collapsible>;
export const QueueSection = ({ export const QueueSection = ({
className, className,
defaultOpen = true, defaultOpen = true,
...props ...props
}: QueueSectionProps) => ( }: QueueSectionProps) => (
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} /> <Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
); );
// QueueSectionTrigger - section header/trigger // QueueSectionTrigger - section header/trigger
export type QueueSectionTriggerProps = ComponentProps<"button">; export type QueueSectionTriggerProps = ComponentProps<"button">;
export const QueueSectionTrigger = ({ export const QueueSectionTrigger = ({
children, children,
className, className,
...props ...props
}: QueueSectionTriggerProps) => ( }: QueueSectionTriggerProps) => (
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<button <button
className={cn( 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", "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, className,
)} )}
type="button" type="button"
{...props} {...props}
> >
{children} {children}
</button> </button>
</CollapsibleTrigger> </CollapsibleTrigger>
); );
// QueueSectionLabel - label content with icon and count // QueueSectionLabel - label content with icon and count
export type QueueSectionLabelProps = ComponentProps<"span"> & { export type QueueSectionLabelProps = ComponentProps<"span"> & {
count?: number; count?: number;
label: string; label: string;
icon?: React.ReactNode; icon?: React.ReactNode;
}; };
export const QueueSectionLabel = ({ export const QueueSectionLabel = ({
count, count,
label, label,
icon, icon,
className, className,
...props ...props
}: QueueSectionLabelProps) => ( }: QueueSectionLabelProps) => (
<span className={cn("flex items-center gap-2", className)} {...props}> <span className={cn("flex items-center gap-2", className)} {...props}>
<ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" /> <ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" />
{icon} {icon}
<span> <span>
{count} {label} {count} {label}
</span> </span>
</span> </span>
); );
// QueueSectionContent - collapsible content area // QueueSectionContent - collapsible content area
export type QueueSectionContentProps = ComponentProps< export type QueueSectionContentProps = ComponentProps<
typeof CollapsibleContent typeof CollapsibleContent
>; >;
export const QueueSectionContent = ({ export const QueueSectionContent = ({
className, className,
...props ...props
}: QueueSectionContentProps) => ( }: QueueSectionContentProps) => (
<CollapsibleContent className={cn(className)} {...props} /> <CollapsibleContent className={cn(className)} {...props} />
); );
export type QueueProps = ComponentProps<"div">; export type QueueProps = ComponentProps<"div">;
export const Queue = ({ className, ...props }: QueueProps) => ( export const Queue = ({ className, ...props }: QueueProps) => (
<div <div
className={cn( className={cn(
"border-border bg-background flex flex-col gap-2 rounded-xl border px-3 pt-2 pb-2 shadow-xs", "border-border bg-background flex flex-col gap-2 rounded-xl border px-3 pt-2 pb-2 shadow-xs",
className, className,
)} )}
{...props} {...props}
/> />
); );
@@ -1,192 +1,192 @@
"use client"; "use client";
import { useControllableState } from "@radix-ui/react-use-controllable-state"; import { useControllableState } from "@radix-ui/react-use-controllable-state";
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { BrainIcon, ChevronDownIcon } from "lucide-react"; import { BrainIcon, ChevronDownIcon } from "lucide-react";
import type { ComponentProps, ReactNode } from "react"; import type { ComponentProps, ReactNode } from "react";
import { createContext, memo, useContext, useEffect, useState } from "react"; import { createContext, memo, useContext, useEffect, useState } from "react";
import { Streamdown } from "streamdown"; import { Streamdown } from "streamdown";
import { reasoningPlugins } from "@/core/streamdown/plugins"; import { reasoningPlugins } from "@/core/streamdown/plugins";
import { Shimmer } from "./shimmer"; import { Shimmer } from "./shimmer";
type ReasoningContextValue = { type ReasoningContextValue = {
isStreaming: boolean; isStreaming: boolean;
isOpen: boolean; isOpen: boolean;
setIsOpen: (open: boolean) => void; setIsOpen: (open: boolean) => void;
duration: number | undefined; duration: number | undefined;
}; };
const ReasoningContext = createContext<ReasoningContextValue | null>(null); const ReasoningContext = createContext<ReasoningContextValue | null>(null);
export const useReasoning = () => { export const useReasoning = () => {
const context = useContext(ReasoningContext); const context = useContext(ReasoningContext);
if (!context) { if (!context) {
throw new Error("Reasoning components must be used within Reasoning"); throw new Error("Reasoning components must be used within Reasoning");
} }
return context; return context;
}; };
export type ReasoningProps = ComponentProps<typeof Collapsible> & { export type ReasoningProps = ComponentProps<typeof Collapsible> & {
isStreaming?: boolean; isStreaming?: boolean;
open?: boolean; open?: boolean;
defaultOpen?: boolean; defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
duration?: number; duration?: number;
}; };
const AUTO_CLOSE_DELAY = 1000; const AUTO_CLOSE_DELAY = 1000;
const MS_IN_S = 1000; const MS_IN_S = 1000;
export const Reasoning = memo( export const Reasoning = memo(
({ ({
className, className,
isStreaming = false, isStreaming = false,
open, open,
defaultOpen = true, defaultOpen = true,
onOpenChange, onOpenChange,
duration: durationProp, duration: durationProp,
children, children,
...props ...props
}: ReasoningProps) => { }: ReasoningProps) => {
const [isOpen, setIsOpen] = useControllableState({ const [isOpen, setIsOpen] = useControllableState({
prop: open, prop: open,
defaultProp: defaultOpen, defaultProp: defaultOpen,
onChange: onOpenChange, onChange: onOpenChange,
}); });
const [duration, setDuration] = useControllableState({ const [duration, setDuration] = useControllableState({
prop: durationProp, prop: durationProp,
defaultProp: undefined, defaultProp: undefined,
}); });
const [hasAutoClosed, setHasAutoClosed] = useState(false); const [hasAutoClosed, setHasAutoClosed] = useState(false);
const [startTime, setStartTime] = useState<number | null>(null); const [startTime, setStartTime] = useState<number | null>(null);
// Track duration when streaming starts and ends // Track duration when streaming starts and ends
useEffect(() => { useEffect(() => {
if (isStreaming) { if (isStreaming) {
if (startTime === null) { if (startTime === null) {
setStartTime(Date.now()); setStartTime(Date.now());
} }
} else if (startTime !== null) { } else if (startTime !== null) {
setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S)); setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S));
setStartTime(null); setStartTime(null);
} }
}, [isStreaming, startTime, setDuration]); }, [isStreaming, startTime, setDuration]);
// Auto-open when streaming starts, auto-close when streaming ends (once only) // Auto-open when streaming starts, auto-close when streaming ends (once only)
useEffect(() => { useEffect(() => {
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) { if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
// Add a small delay before closing to allow user to see the content // Add a small delay before closing to allow user to see the content
const timer = setTimeout(() => { const timer = setTimeout(() => {
setIsOpen(false); setIsOpen(false);
setHasAutoClosed(true); setHasAutoClosed(true);
}, AUTO_CLOSE_DELAY); }, AUTO_CLOSE_DELAY);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed]); }, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed]);
const handleOpenChange = (newOpen: boolean) => { const handleOpenChange = (newOpen: boolean) => {
setIsOpen(newOpen); setIsOpen(newOpen);
}; };
return ( return (
<ReasoningContext.Provider <ReasoningContext.Provider
value={{ isStreaming, isOpen, setIsOpen, duration }} value={{ isStreaming, isOpen, setIsOpen, duration }}
> >
<Collapsible <Collapsible
className={cn("not-prose mb-4", className)} className={cn("not-prose mb-4", className)}
onOpenChange={handleOpenChange} onOpenChange={handleOpenChange}
open={isOpen} open={isOpen}
{...props} {...props}
> >
{children} {children}
</Collapsible> </Collapsible>
</ReasoningContext.Provider> </ReasoningContext.Provider>
); );
}, },
); );
export type ReasoningTriggerProps = ComponentProps< export type ReasoningTriggerProps = ComponentProps<
typeof CollapsibleTrigger typeof CollapsibleTrigger
> & { > & {
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode; getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
}; };
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => { const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
if (isStreaming || duration === 0) { if (isStreaming || duration === 0) {
return <Shimmer duration={1}>Thinking...</Shimmer>; return <Shimmer duration={1}>Thinking...</Shimmer>;
} }
if (duration === undefined) { if (duration === undefined) {
return <span>Thought for a few seconds</span>; return <span>Thought for a few seconds</span>;
} }
return <span>Thought for {duration} seconds</span>; return <span>Thought for {duration} seconds</span>;
}; };
export const ReasoningTrigger = memo( export const ReasoningTrigger = memo(
({ ({
className, className,
children, children,
getThinkingMessage = defaultGetThinkingMessage, getThinkingMessage = defaultGetThinkingMessage,
...props ...props
}: ReasoningTriggerProps) => { }: ReasoningTriggerProps) => {
const { isStreaming, isOpen, duration } = useReasoning(); const { isStreaming, isOpen, duration } = useReasoning();
return ( return (
<CollapsibleTrigger <CollapsibleTrigger
className={cn( className={cn(
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors", "text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
className, className,
)} )}
{...props} {...props}
> >
{children ?? ( {children ?? (
<> <>
<BrainIcon className="size-4" /> <BrainIcon className="size-4" />
{getThinkingMessage(isStreaming, duration)} {getThinkingMessage(isStreaming, duration)}
<ChevronDownIcon <ChevronDownIcon
className={cn( className={cn(
"size-4 transition-transform", "size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0", isOpen ? "rotate-180" : "rotate-0",
)} )}
/> />
</> </>
)} )}
</CollapsibleTrigger> </CollapsibleTrigger>
); );
}, },
); );
export type ReasoningContentProps = ComponentProps< export type ReasoningContentProps = ComponentProps<
typeof CollapsibleContent typeof CollapsibleContent
> & { > & {
children: ReactNode; children: ReactNode;
}; };
export const ReasoningContent = memo( export const ReasoningContent = memo(
({ className, children, ...props }: ReasoningContentProps) => ( ({ className, children, ...props }: ReasoningContentProps) => (
<CollapsibleContent <CollapsibleContent
className={cn( className={cn(
"mt-4 text-sm", "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", "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, className,
)} )}
{...props} {...props}
> >
{typeof children === "string" ? ( {typeof children === "string" ? (
<Streamdown {...reasoningPlugins}>{children}</Streamdown> <Streamdown {...reasoningPlugins}>{children}</Streamdown>
) : ( ) : (
children children
)} )}
</CollapsibleContent> </CollapsibleContent>
), ),
); );
Reasoning.displayName = "Reasoning"; Reasoning.displayName = "Reasoning";
ReasoningTrigger.displayName = "ReasoningTrigger"; ReasoningTrigger.displayName = "ReasoningTrigger";
ReasoningContent.displayName = "ReasoningContent"; ReasoningContent.displayName = "ReasoningContent";
@@ -1,64 +1,64 @@
"use client"; "use client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { import {
type CSSProperties, type CSSProperties,
type ElementType, type ElementType,
type JSX, type JSX,
memo, memo,
useMemo, useMemo,
} from "react"; } from "react";
export type TextShimmerProps = { export type TextShimmerProps = {
children: string; children: string;
as?: ElementType; as?: ElementType;
className?: string; className?: string;
duration?: number; duration?: number;
spread?: number; spread?: number;
}; };
const ShimmerComponent = ({ const ShimmerComponent = ({
children, children,
as: Component = "p", as: Component = "p",
className, className,
duration = 2, duration = 2,
spread = 2, spread = 2,
}: TextShimmerProps) => { }: TextShimmerProps) => {
const MotionComponent = motion.create( const MotionComponent = motion.create(
Component as keyof JSX.IntrinsicElements, Component as keyof JSX.IntrinsicElements,
); );
const dynamicSpread = useMemo( const dynamicSpread = useMemo(
() => (children?.length ?? 0) * spread, () => (children?.length ?? 0) * spread,
[children, spread], [children, spread],
); );
return ( return (
<MotionComponent <MotionComponent
animate={{ backgroundPosition: "0% center" }} animate={{ backgroundPosition: "0% center" }}
className={cn( className={cn(
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent", "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)))]", "[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))]",
className, className,
)} )}
initial={{ backgroundPosition: "100% center" }} initial={{ backgroundPosition: "100% center" }}
style={ style={
{ {
"--spread": `${dynamicSpread}px`, "--spread": `${dynamicSpread}px`,
backgroundImage: backgroundImage:
"var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))", "var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))",
} as CSSProperties } as CSSProperties
} }
transition={{ transition={{
repeat: Number.POSITIVE_INFINITY, repeat: Number.POSITIVE_INFINITY,
duration, duration,
ease: "linear", ease: "linear",
}} }}
> >
{children} {children}
</MotionComponent> </MotionComponent>
); );
}; };
export const Shimmer = memo(ShimmerComponent); export const Shimmer = memo(ShimmerComponent);
@@ -1,77 +1,77 @@
"use client"; "use client";
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { BookIcon, ChevronDownIcon } from "lucide-react"; import { BookIcon, ChevronDownIcon } from "lucide-react";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
export type SourcesProps = ComponentProps<"div">; export type SourcesProps = ComponentProps<"div">;
export const Sources = ({ className, ...props }: SourcesProps) => ( export const Sources = ({ className, ...props }: SourcesProps) => (
<Collapsible <Collapsible
className={cn("not-prose text-primary mb-4 text-xs", className)} className={cn("not-prose text-primary mb-4 text-xs", className)}
{...props} {...props}
/> />
); );
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & { export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
count: number; count: number;
}; };
export const SourcesTrigger = ({ export const SourcesTrigger = ({
className, className,
count, count,
children, children,
...props ...props
}: SourcesTriggerProps) => ( }: SourcesTriggerProps) => (
<CollapsibleTrigger <CollapsibleTrigger
className={cn("flex items-center gap-2", className)} className={cn("flex items-center gap-2", className)}
{...props} {...props}
> >
{children ?? ( {children ?? (
<> <>
<p className="font-medium">Used {count} sources</p> <p className="font-medium">Used {count} sources</p>
<ChevronDownIcon className="h-4 w-4" /> <ChevronDownIcon className="h-4 w-4" />
</> </>
)} )}
</CollapsibleTrigger> </CollapsibleTrigger>
); );
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>; export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
export const SourcesContent = ({ export const SourcesContent = ({
className, className,
...props ...props
}: SourcesContentProps) => ( }: SourcesContentProps) => (
<CollapsibleContent <CollapsibleContent
className={cn( className={cn(
"mt-3 flex w-fit flex-col gap-2", "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", "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, className,
)} )}
{...props} {...props}
/> />
); );
export type SourceProps = ComponentProps<"a">; export type SourceProps = ComponentProps<"a">;
export const Source = ({ href, title, children, ...props }: SourceProps) => ( export const Source = ({ href, title, children, ...props }: SourceProps) => (
<a <a
className="flex items-center gap-2" className="flex items-center gap-2"
href={href} href={href}
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
{...props} {...props}
> >
{children ?? ( {children ?? (
<> <>
<BookIcon className="h-4 w-4" /> <BookIcon className="h-4 w-4" />
<span className="block font-medium">{title}</span> <span className="block font-medium">{title}</span>
</> </>
)} )}
</a> </a>
); );
@@ -1,80 +1,80 @@
"use client"; "use client";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import { Children, type ComponentProps } from "react"; import { Children, type ComponentProps } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const STAGGER_DELAY_MS = 60; const STAGGER_DELAY_MS = 60;
const STAGGER_DELAY_MS_OFFSET = 250; const STAGGER_DELAY_MS_OFFSET = 250;
export type SuggestionsProps = ComponentProps<typeof ScrollArea>; export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
export const Suggestions = ({ export const Suggestions = ({
className, className,
children, children,
...props ...props
}: SuggestionsProps) => ( }: SuggestionsProps) => (
<ScrollArea className="overflow-x-auto whitespace-normal" {...props}> <ScrollArea className="overflow-x-auto whitespace-normal" {...props}>
<div <div
className={cn("flex w-full flex-wrap items-center gap-2", className)} className={cn("flex w-full flex-wrap items-center gap-2", className)}
data-slot="suggestions-list" data-slot="suggestions-list"
> >
{Children.map(children, (child, index) => {Children.map(children, (child, index) =>
child != null ? ( child != null ? (
<span <span
className="animate-fade-in-up max-w-full opacity-0" className="animate-fade-in-up max-w-full opacity-0"
style={{ style={{
animationDelay: `${STAGGER_DELAY_MS_OFFSET + index * STAGGER_DELAY_MS}ms`, animationDelay: `${STAGGER_DELAY_MS_OFFSET + index * STAGGER_DELAY_MS}ms`,
}} }}
> >
{child} {child}
</span> </span>
) : ( ) : (
child child
), ),
)} )}
</div> </div>
<ScrollBar className="hidden" orientation="horizontal" /> <ScrollBar className="hidden" orientation="horizontal" />
</ScrollArea> </ScrollArea>
); );
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & { export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
suggestion: React.ReactNode; suggestion: React.ReactNode;
icon?: LucideIcon; icon?: LucideIcon;
onClick?: () => void; onClick?: () => void;
}; };
export const Suggestion = ({ export const Suggestion = ({
suggestion, suggestion,
onClick, onClick,
className, className,
icon: Icon, icon: Icon,
variant = "outline", variant = "outline",
size = "sm", size = "sm",
children, children,
...props ...props
}: SuggestionProps) => { }: SuggestionProps) => {
const handleClick = () => { const handleClick = () => {
onClick?.(); onClick?.();
}; };
return ( return (
<Button <Button
className={cn( 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", "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, className,
)} )}
onClick={handleClick} onClick={handleClick}
size={size} size={size}
type="button" type="button"
variant={variant} variant={variant}
{...props} {...props}
> >
{Icon && <Icon className="size-4" />} {Icon && <Icon className="size-4" />}
{children ?? suggestion} {children ?? suggestion}
</Button> </Button>
); );
}; };
@@ -1,87 +1,87 @@
"use client"; "use client";
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ChevronDownIcon, SearchIcon } from "lucide-react"; import { ChevronDownIcon, SearchIcon } from "lucide-react";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
export type TaskItemFileProps = ComponentProps<"div">; export type TaskItemFileProps = ComponentProps<"div">;
export const TaskItemFile = ({ export const TaskItemFile = ({
children, children,
className, className,
...props ...props
}: TaskItemFileProps) => ( }: TaskItemFileProps) => (
<div <div
className={cn( className={cn(
"bg-secondary text-foreground inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs", "bg-secondary text-foreground inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
</div> </div>
); );
export type TaskItemProps = ComponentProps<"div">; export type TaskItemProps = ComponentProps<"div">;
export const TaskItem = ({ children, className, ...props }: TaskItemProps) => ( export const TaskItem = ({ children, className, ...props }: TaskItemProps) => (
<div className={cn("text-muted-foreground text-sm", className)} {...props}> <div className={cn("text-muted-foreground text-sm", className)} {...props}>
{children} {children}
</div> </div>
); );
export type TaskProps = ComponentProps<typeof Collapsible>; export type TaskProps = ComponentProps<typeof Collapsible>;
export const Task = ({ export const Task = ({
defaultOpen = true, defaultOpen = true,
className, className,
...props ...props
}: TaskProps) => ( }: TaskProps) => (
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} /> <Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
); );
export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & { export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
title: string; title: string;
}; };
export const TaskTrigger = ({ export const TaskTrigger = ({
children, children,
className, className,
title, title,
...props ...props
}: TaskTriggerProps) => ( }: TaskTriggerProps) => (
<CollapsibleTrigger asChild className={cn("group", className)} {...props}> <CollapsibleTrigger asChild className={cn("group", className)} {...props}>
{children ?? ( {children ?? (
<div className="text-muted-foreground hover:text-foreground flex w-full cursor-pointer items-center gap-2 text-sm transition-colors"> <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" /> <SearchIcon className="size-4" />
<p className="text-sm">{title}</p> <p className="text-sm">{title}</p>
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" /> <ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
</div> </div>
)} )}
</CollapsibleTrigger> </CollapsibleTrigger>
); );
export type TaskContentProps = ComponentProps<typeof CollapsibleContent>; export type TaskContentProps = ComponentProps<typeof CollapsibleContent>;
export const TaskContent = ({ export const TaskContent = ({
children, children,
className, className,
...props ...props
}: TaskContentProps) => ( }: TaskContentProps) => (
<CollapsibleContent <CollapsibleContent
className={cn( 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", "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, className,
)} )}
{...props} {...props}
> >
<div className="border-muted mt-4 space-y-2 border-l-2 pl-4"> <div className="border-muted mt-4 space-y-2 border-l-2 pl-4">
{children} {children}
</div> </div>
</CollapsibleContent> </CollapsibleContent>
); );
@@ -1,16 +1,16 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { NodeToolbar, Position } from "@xyflow/react"; import { NodeToolbar, Position } from "@xyflow/react";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
type ToolbarProps = ComponentProps<typeof NodeToolbar>; type ToolbarProps = ComponentProps<typeof NodeToolbar>;
export const Toolbar = ({ className, ...props }: ToolbarProps) => ( export const Toolbar = ({ className, ...props }: ToolbarProps) => (
<NodeToolbar <NodeToolbar
className={cn( className={cn(
"bg-background flex items-center gap-1 rounded-sm border p-1.5", "bg-background flex items-center gap-1 rounded-sm border p-1.5",
className, className,
)} )}
position={Position.Bottom} position={Position.Bottom}
{...props} {...props}
/> />
); );
@@ -1,263 +1,263 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ChevronDownIcon } from "lucide-react"; import { ChevronDownIcon } from "lucide-react";
import type { ComponentProps, ReactNode } from "react"; import type { ComponentProps, ReactNode } from "react";
import { createContext, useContext, useEffect, useState } from "react"; import { createContext, useContext, useEffect, useState } from "react";
export type WebPreviewContextValue = { export type WebPreviewContextValue = {
url: string; url: string;
setUrl: (url: string) => void; setUrl: (url: string) => void;
consoleOpen: boolean; consoleOpen: boolean;
setConsoleOpen: (open: boolean) => void; setConsoleOpen: (open: boolean) => void;
}; };
const WebPreviewContext = createContext<WebPreviewContextValue | null>(null); const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);
const useWebPreview = () => { const useWebPreview = () => {
const context = useContext(WebPreviewContext); const context = useContext(WebPreviewContext);
if (!context) { if (!context) {
throw new Error("WebPreview components must be used within a WebPreview"); throw new Error("WebPreview components must be used within a WebPreview");
} }
return context; return context;
}; };
export type WebPreviewProps = ComponentProps<"div"> & { export type WebPreviewProps = ComponentProps<"div"> & {
defaultUrl?: string; defaultUrl?: string;
onUrlChange?: (url: string) => void; onUrlChange?: (url: string) => void;
}; };
export const WebPreview = ({ export const WebPreview = ({
className, className,
children, children,
defaultUrl = "", defaultUrl = "",
onUrlChange, onUrlChange,
...props ...props
}: WebPreviewProps) => { }: WebPreviewProps) => {
const [url, setUrl] = useState(defaultUrl); const [url, setUrl] = useState(defaultUrl);
const [consoleOpen, setConsoleOpen] = useState(false); const [consoleOpen, setConsoleOpen] = useState(false);
const handleUrlChange = (newUrl: string) => { const handleUrlChange = (newUrl: string) => {
setUrl(newUrl); setUrl(newUrl);
onUrlChange?.(newUrl); onUrlChange?.(newUrl);
}; };
const contextValue: WebPreviewContextValue = { const contextValue: WebPreviewContextValue = {
url, url,
setUrl: handleUrlChange, setUrl: handleUrlChange,
consoleOpen, consoleOpen,
setConsoleOpen, setConsoleOpen,
}; };
return ( return (
<WebPreviewContext.Provider value={contextValue}> <WebPreviewContext.Provider value={contextValue}>
<div <div
className={cn( className={cn(
"bg-card flex size-full flex-col rounded-lg border", "bg-card flex size-full flex-col rounded-lg border",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
</div> </div>
</WebPreviewContext.Provider> </WebPreviewContext.Provider>
); );
}; };
export type WebPreviewNavigationProps = ComponentProps<"div">; export type WebPreviewNavigationProps = ComponentProps<"div">;
export const WebPreviewNavigation = ({ export const WebPreviewNavigation = ({
className, className,
children, children,
...props ...props
}: WebPreviewNavigationProps) => ( }: WebPreviewNavigationProps) => (
<div <div
className={cn("flex items-center gap-1 border-b p-2", className)} className={cn("flex items-center gap-1 border-b p-2", className)}
{...props} {...props}
> >
{children} {children}
</div> </div>
); );
export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & { export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & {
tooltip?: string; tooltip?: string;
}; };
export const WebPreviewNavigationButton = ({ export const WebPreviewNavigationButton = ({
onClick, onClick,
disabled, disabled,
tooltip, tooltip,
children, children,
...props ...props
}: WebPreviewNavigationButtonProps) => ( }: WebPreviewNavigationButtonProps) => (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
className="hover:text-foreground h-8 w-8 p-0" className="hover:text-foreground h-8 w-8 p-0"
disabled={disabled} disabled={disabled}
onClick={onClick} onClick={onClick}
size="sm" size="sm"
variant="ghost" variant="ghost"
{...props} {...props}
> >
{children} {children}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{tooltip}</p> <p>{tooltip}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
); );
export type WebPreviewUrlProps = ComponentProps<typeof Input>; export type WebPreviewUrlProps = ComponentProps<typeof Input>;
export const WebPreviewUrl = ({ export const WebPreviewUrl = ({
value, value,
onChange, onChange,
onKeyDown, onKeyDown,
...props ...props
}: WebPreviewUrlProps) => { }: WebPreviewUrlProps) => {
const { url, setUrl } = useWebPreview(); const { url, setUrl } = useWebPreview();
const [inputValue, setInputValue] = useState(url); const [inputValue, setInputValue] = useState(url);
// Sync input value with context URL when it changes externally // Sync input value with context URL when it changes externally
useEffect(() => { useEffect(() => {
setInputValue(url); setInputValue(url);
}, [url]); }, [url]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value); setInputValue(event.target.value);
onChange?.(event); onChange?.(event);
}; };
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") { if (event.key === "Enter") {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
setUrl(target.value); setUrl(target.value);
} }
onKeyDown?.(event); onKeyDown?.(event);
}; };
return ( return (
<Input <Input
className="h-8 flex-1 text-sm" className="h-8 flex-1 text-sm"
onChange={onChange ?? handleChange} onChange={onChange ?? handleChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Enter URL..." placeholder="Enter URL..."
value={value ?? inputValue} value={value ?? inputValue}
{...props} {...props}
/> />
); );
}; };
export type WebPreviewBodyProps = ComponentProps<"iframe"> & { export type WebPreviewBodyProps = ComponentProps<"iframe"> & {
loading?: ReactNode; loading?: ReactNode;
}; };
export const WebPreviewBody = ({ export const WebPreviewBody = ({
className, className,
loading, loading,
src, src,
...props ...props
}: WebPreviewBodyProps) => { }: WebPreviewBodyProps) => {
const { url } = useWebPreview(); const { url } = useWebPreview();
return ( return (
<div className="flex-1"> <div className="flex-1">
<iframe <iframe
className={cn("size-full", className)} className={cn("size-full", className)}
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
src={(src ?? url) || undefined} src={(src ?? url) || undefined}
title="Preview" title="Preview"
{...props} {...props}
/> />
{loading} {loading}
</div> </div>
); );
}; };
export type WebPreviewConsoleProps = ComponentProps<"div"> & { export type WebPreviewConsoleProps = ComponentProps<"div"> & {
logs?: Array<{ logs?: Array<{
level: "log" | "warn" | "error"; level: "log" | "warn" | "error";
message: string; message: string;
timestamp: Date; timestamp: Date;
}>; }>;
}; };
export const WebPreviewConsole = ({ export const WebPreviewConsole = ({
className, className,
logs = [], logs = [],
children, children,
...props ...props
}: WebPreviewConsoleProps) => { }: WebPreviewConsoleProps) => {
const { consoleOpen, setConsoleOpen } = useWebPreview(); const { consoleOpen, setConsoleOpen } = useWebPreview();
return ( return (
<Collapsible <Collapsible
className={cn("bg-muted/50 border-t font-mono text-sm", className)} className={cn("bg-muted/50 border-t font-mono text-sm", className)}
onOpenChange={setConsoleOpen} onOpenChange={setConsoleOpen}
open={consoleOpen} open={consoleOpen}
{...props} {...props}
> >
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button <Button
className="hover:bg-muted/50 flex w-full items-center justify-between p-4 text-left font-medium" className="hover:bg-muted/50 flex w-full items-center justify-between p-4 text-left font-medium"
variant="ghost" variant="ghost"
> >
Console Console
<ChevronDownIcon <ChevronDownIcon
className={cn( className={cn(
"h-4 w-4 transition-transform duration-200", "h-4 w-4 transition-transform duration-200",
consoleOpen && "rotate-180", consoleOpen && "rotate-180",
)} )}
/> />
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent <CollapsibleContent
className={cn( className={cn(
"px-4 pb-4", "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", "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"> <div className="max-h-48 space-y-1 overflow-y-auto">
{logs.length === 0 ? ( {logs.length === 0 ? (
<p className="text-muted-foreground">No console output</p> <p className="text-muted-foreground">No console output</p>
) : ( ) : (
logs.map((log, index) => ( logs.map((log, index) => (
<div <div
className={cn( className={cn(
"text-xs", "text-xs",
log.level === "error" && "text-destructive", log.level === "error" && "text-destructive",
log.level === "warn" && "text-yellow-600", log.level === "warn" && "text-yellow-600",
log.level === "log" && "text-foreground", log.level === "log" && "text-foreground",
)} )}
key={`${log.timestamp.getTime()}-${index}`} key={`${log.timestamp.getTime()}-${index}`}
> >
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{log.timestamp.toLocaleTimeString()} {log.timestamp.toLocaleTimeString()}
</span>{" "} </span>{" "}
{log.message} {log.message}
</div> </div>
)) ))
)} )}
{children} {children}
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
); );
}; };
+19 -19
View File
@@ -1,19 +1,19 @@
import { useMemo } from "react"; import { useMemo } from "react";
export function Footer() { export function Footer() {
const year = useMemo(() => new Date().getFullYear(), []); const year = useMemo(() => new Date().getFullYear(), []);
return ( return (
<footer className="container-md mx-auto mt-32 flex flex-col items-center justify-center"> <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" /> <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"> <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"> <p className="text-center font-serif text-lg md:text-xl">
&quot;Originated from Open Source, give back to Open Source.&quot; &quot;Originated from Open Source, give back to Open Source.&quot;
</p> </p>
</div> </div>
<div className="text-muted-foreground container mb-8 flex flex-col items-center justify-center text-xs"> <div className="text-muted-foreground container mb-8 flex flex-col items-center justify-center text-xs">
<p>Licensed under MIT License</p> <p>Licensed under MIT License</p>
<p>&copy; {year} CM 助手</p> <p>&copy; {year} CM 助手</p>
</div> </div>
</footer> </footer>
); );
} }
+52 -52
View File
@@ -1,52 +1,52 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import type { Locale } from "@/core/i18n/locale"; import type { Locale } from "@/core/i18n/locale";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export type HeaderProps = { export type HeaderProps = {
className?: string; className?: string;
homeURL?: string; homeURL?: string;
locale?: Locale; locale?: Locale;
}; };
export function Header({ className, homeURL }: HeaderProps) { export function Header({ className, homeURL }: HeaderProps) {
const isExternalHome = !homeURL; const isExternalHome = !homeURL;
const { locale: resolvedLocale, t } = useI18n(); const { locale: resolvedLocale, t } = useI18n();
const lang = resolvedLocale.substring(0, 2); const lang = resolvedLocale.substring(0, 2);
return ( return (
<header <header
className={cn( 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", "container-md fixed top-0 right-0 left-0 z-20 mx-auto flex h-16 items-center justify-between backdrop-blur-xs",
className, className,
)} )}
> >
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<a <a
href={homeURL ?? "/"} href={homeURL ?? "/"}
target={isExternalHome ? "_blank" : "_self"} target={isExternalHome ? "_blank" : "_self"}
rel={isExternalHome ? "noopener noreferrer" : undefined} rel={isExternalHome ? "noopener noreferrer" : undefined}
> >
<h1 className="font-serif text-xl">CM 助手</h1> <h1 className="font-serif text-xl">CM 助手</h1>
</a> </a>
</div> </div>
<nav className="mr-8 ml-auto flex items-center gap-8 text-sm font-medium"> <nav className="mr-8 ml-auto flex items-center gap-8 text-sm font-medium">
<Link <Link
href={`/${lang}/docs`} href={`/${lang}/docs`}
className="text-secondary-foreground hover:text-foreground transition-colors" className="text-secondary-foreground hover:text-foreground transition-colors"
> >
{t.home.docs} {t.home.docs}
</Link> </Link>
<Link <Link
href="/blog/posts" href="/blog/posts"
className="text-secondary-foreground hover:text-foreground transition-colors" className="text-secondary-foreground hover:text-foreground transition-colors"
> >
{t.home.blog} {t.home.blog}
</Link> </Link>
</nav> </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" /> <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> </header>
); );
} }
+141 -141
View File
@@ -1,141 +1,141 @@
"use client"; "use client";
import { ChevronRightIcon } from "lucide-react"; import { ChevronRightIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import deerMaskUrl from "@/assets/deer.svg"; import deerMaskUrl from "@/assets/deer.svg";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { FlickeringGrid } from "@/components/ui/flickering-grid"; import { FlickeringGrid } from "@/components/ui/flickering-grid";
import Galaxy from "@/components/ui/galaxy"; import Galaxy from "@/components/ui/galaxy";
import { WordRotate } from "@/components/ui/word-rotate"; import { WordRotate } from "@/components/ui/word-rotate";
import { env } from "@/env"; import { env } from "@/env";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export function Hero({ className }: { className?: string }) { export function Hero({ className }: { className?: string }) {
return ( return (
<div <div
className={cn( className={cn(
"flex size-full flex-col items-center justify-center", "flex size-full flex-col items-center justify-center",
className, className,
)} )}
> >
<div className="absolute inset-0 z-0 bg-black/40"> <div className="absolute inset-0 z-0 bg-black/40">
<Galaxy <Galaxy
mouseRepulsion={false} mouseRepulsion={false}
starSpeed={0.2} starSpeed={0.2}
density={0.6} density={0.6}
glowIntensity={0.35} glowIntensity={0.35}
twinkleIntensity={0.3} twinkleIntensity={0.3}
speed={0.5} speed={0.5}
/> />
</div> </div>
<FlickeringGrid <FlickeringGrid
className="absolute inset-0 z-0 translate-y-8 mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]" className="absolute inset-0 z-0 translate-y-8 mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
style={{ style={{
WebkitMaskImage: `url("${deerMaskUrl}")`, WebkitMaskImage: `url("${deerMaskUrl}")`,
maskImage: `url("${deerMaskUrl}")`, maskImage: `url("${deerMaskUrl}")`,
}} }}
squareSize={4} squareSize={4}
gridGap={4} gridGap={4}
color={"white"} color={"white"}
maxOpacity={0.3} maxOpacity={0.3}
flickerChance={0.25} flickerChance={0.25}
/> />
<div className="container-md relative z-10 mx-auto flex h-screen flex-col items-center justify-center"> <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"> <h1 className="flex items-center gap-2 text-4xl font-bold md:text-6xl">
<WordRotate <WordRotate
words={[ words={[
"Deep Research", "Deep Research",
"Collect Data", "Collect Data",
"Analyze Data", "Analyze Data",
"Generate Webpages", "Generate Webpages",
"Vibe Coding", "Vibe Coding",
"Generate Slides", "Generate Slides",
"Generate Images", "Generate Images",
"Generate Podcasts", "Generate Podcasts",
"Generate Videos", "Generate Videos",
"Generate Songs", "Generate Songs",
"Organize Emails", "Organize Emails",
"Do Anything", "Do Anything",
"Learn Anything", "Learn Anything",
]} ]}
/>{" "} />{" "}
<div>CM 助手</div> <div>CM 助手</div>
</h1> </h1>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && ( {env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
<a <a
href="https://byteplus.com" href="https://byteplus.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<div className="mt-4 flex size-fit items-center rounded-lg border border-white px-3 text-center text-shadow-sm"> <div className="mt-4 flex size-fit items-center rounded-lg border border-white px-3 text-center text-shadow-sm">
<span>In partnership with</span>&nbsp; <span>In partnership with</span>&nbsp;
<BytePlusIcon className="h-4" /> <BytePlusIcon className="h-4" />
</div> </div>
</a> </a>
)} )}
<p className="text-muted-foreground mt-8 scale-105 text-center text-2xl text-shadow-sm"> <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. An open-source SuperAgent harness that researches, codes, and creates.
With With
<br /> <br />
the help of sandboxes, memories, tools, skills and subagents, it the help of sandboxes, memories, tools, skills and subagents, it
handles handles
<br /> <br />
different levels of tasks that could take minutes to hours. different levels of tasks that could take minutes to hours.
</p> </p>
<Link href="/workspace"> <Link href="/workspace">
<Button className="size-lg mt-8 scale-108" size="lg"> <Button className="size-lg mt-8 scale-108" size="lg">
<span className="text-md">Get Started with 2.0</span> <span className="text-md">Get Started with 2.0</span>
<ChevronRightIcon className="size-4" /> <ChevronRightIcon className="size-4" />
</Button> </Button>
</Link> </Link>
</div> </div>
</div> </div>
); );
} }
function BytePlusIcon(props: React.SVGProps<SVGSVGElement>) { function BytePlusIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg
width={80} width={80}
height={24} height={24}
viewBox="0 0 120 24" viewBox="0 0 120 24"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
> >
<path <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" 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" fill="currentColor"
/> />
<path <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" 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" fill="currentColor"
/> />
<path <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" 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" fill="currentColor"
/> />
<path <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" 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" fill="currentColor"
/> />
<path <path
d="M92.168 3.36719H94.8104V18.8101H92.168V3.36719Z" d="M92.168 3.36719H94.8104V18.8101H92.168V3.36719Z"
fill="currentColor" fill="currentColor"
/> />
<path <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" 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" fill="currentColor"
/> />
<path <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" 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" fill="currentColor"
/> />
<path <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" 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" fill="currentColor"
/> />
</svg> </svg>
); );
} }
+163 -163
View File
@@ -1,163 +1,163 @@
import Link from "next/link"; import Link from "next/link";
import { getBlogRoute, normalizeTagSlug, type BlogPost } from "@/core/blog"; import { getBlogRoute, normalizeTagSlug, type BlogPost } from "@/core/blog";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
type PostListProps = { type PostListProps = {
description?: string; description?: string;
posts: BlogPost[]; posts: BlogPost[];
title: string; title: string;
}; };
type PostMetaProps = { type PostMetaProps = {
currentLang?: string; currentLang?: string;
date?: string | null; date?: string | null;
languages?: string[]; languages?: string[];
pathname?: string; pathname?: string;
}; };
function formatDate(date?: string): string | null { function formatDate(date?: string): string | null {
if (!date) { if (!date) {
return null; return null;
} }
const value = new Date(date); const value = new Date(date);
if (Number.isNaN(value.getTime())) { if (Number.isNaN(value.getTime())) {
return date; return date;
} }
return new Intl.DateTimeFormat("en-US", { return new Intl.DateTimeFormat("en-US", {
day: "numeric", day: "numeric",
month: "short", month: "short",
year: "numeric", year: "numeric",
}).format(value); }).format(value);
} }
export function PostMeta({ export function PostMeta({
currentLang, currentLang,
date, date,
languages, languages,
pathname, pathname,
}: PostMetaProps) { }: PostMetaProps) {
const formattedDate = formatDate(date ?? undefined); const formattedDate = formatDate(date ?? undefined);
const availableLanguages = Array.isArray(languages) const availableLanguages = Array.isArray(languages)
? languages.filter((lang): lang is string => typeof lang === "string") ? languages.filter((lang): lang is string => typeof lang === "string")
: []; : [];
if (!formattedDate && availableLanguages.length <= 1) { if (!formattedDate && availableLanguages.length <= 1) {
return null; return null;
} }
return ( return (
<div className="flex flex-wrap items-center gap-8 text-sm"> <div className="flex flex-wrap items-center gap-8 text-sm">
{formattedDate ? ( {formattedDate ? (
<p className="text-muted-foreground">{formattedDate}</p> <p className="text-muted-foreground">{formattedDate}</p>
) : null} ) : null}
{pathname && availableLanguages.length > 1 ? ( {pathname && availableLanguages.length > 1 ? (
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<span className="text-secondary-foreground text-sm">Language:</span> <span className="text-secondary-foreground text-sm">Language:</span>
{availableLanguages.map((lang) => { {availableLanguages.map((lang) => {
const isActive = lang === currentLang; const isActive = lang === currentLang;
return ( return (
<Link <Link
key={lang} key={lang}
href={`${pathname}?lang=${lang}`} href={`${pathname}?lang=${lang}`}
className={ className={
isActive isActive
? "text-foreground text-sm font-medium" ? "text-foreground text-sm font-medium"
: "text-muted-foreground hover:text-foreground text-sm transition-colors" : "text-muted-foreground hover:text-foreground text-sm transition-colors"
} }
> >
{lang.toUpperCase()} {lang.toUpperCase()}
</Link> </Link>
); );
})} })}
</div> </div>
) : null} ) : null}
</div> </div>
); );
} }
export function PostTags({ export function PostTags({
tags, tags,
className, className,
}: { }: {
tags?: unknown; tags?: unknown;
className?: string; className?: string;
}) { }) {
if (!Array.isArray(tags)) { if (!Array.isArray(tags)) {
return null; return null;
} }
const validTags = tags.filter( const validTags = tags.filter(
(tag): tag is string => typeof tag === "string" && tag.length > 0, (tag): tag is string => typeof tag === "string" && tag.length > 0,
); );
if (validTags.length === 0) { if (validTags.length === 0) {
return null; return null;
} }
return ( return (
<div className={cn("flex flex-wrap items-center gap-3", className)}> <div className={cn("flex flex-wrap items-center gap-3", className)}>
<span className="text-secondary-foreground text-sm">Tags:</span> <span className="text-secondary-foreground text-sm">Tags:</span>
{validTags.map((tag) => ( {validTags.map((tag) => (
<Link <Link
key={tag} key={tag}
href={`/blog/tags/${normalizeTagSlug(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" className="border-border text-secondary-foreground hover:text-foreground rounded-xl border px-2 py-1 text-sm transition-colors"
> >
{tag} {tag}
</Link> </Link>
))} ))}
</div> </div>
); );
} }
export function PostList({ description, posts, title }: PostListProps) { export function PostList({ description, posts, title }: PostListProps) {
return ( return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-12 px-6"> <div className="mx-auto flex w-full max-w-5xl flex-col gap-12 px-6">
<header className="space-y-4"> <header className="space-y-4">
<h2 className="text-foreground text-4xl font-semibold tracking-tight"> <h2 className="text-foreground text-4xl font-semibold tracking-tight">
{title} {title}
</h2> </h2>
{description ? ( {description ? (
<p className="text-secondary-foreground">{description}</p> <p className="text-secondary-foreground">{description}</p>
) : null} ) : null}
</header> </header>
<div className="space-y-12"> <div className="space-y-12">
{posts.map((post) => { {posts.map((post) => {
return ( return (
<article <article
key={post.slug.join("/")} key={post.slug.join("/")}
className="border-border space-y-5 border-b pb-12 last:border-b-0 last:pb-0" className="border-border space-y-5 border-b pb-12 last:border-b-0 last:pb-0"
> >
<div className="space-y-3"> <div className="space-y-3">
<PostMeta <PostMeta
currentLang={post.lang} currentLang={post.lang}
date={post.metadata.date} date={post.metadata.date}
languages={post.languages} languages={post.languages}
pathname={getBlogRoute(post.slug)} pathname={getBlogRoute(post.slug)}
/> />
<Link <Link
href={getBlogRoute(post.slug)} href={getBlogRoute(post.slug)}
className="text-foreground hover:text-primary block text-2xl font-semibold tracking-tight transition-colors" className="text-foreground hover:text-primary block text-2xl font-semibold tracking-tight transition-colors"
> >
{post.title} {post.title}
</Link> </Link>
</div> </div>
{post.metadata.description ? ( {post.metadata.description ? (
<p className="text-secondary-foreground leading-10"> <p className="text-secondary-foreground leading-10">
{post.metadata.description} {post.metadata.description}
</p> </p>
) : null} ) : null}
<PostTags tags={post.metadata.tags} /> <PostTags tags={post.metadata.tags} />
</article> </article>
); );
})} })}
</div> </div>
</div> </div>
); );
} }
File diff suppressed because it is too large. Load diff
+29 -29
View File
@@ -1,29 +1,29 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export function Section({ export function Section({
className, className,
title, title,
subtitle, subtitle,
children, children,
}: { }: {
className?: string; className?: string;
title: React.ReactNode; title: React.ReactNode;
subtitle?: React.ReactNode; subtitle?: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<section className={cn("mx-auto flex flex-col py-16", className)}> <section className={cn("mx-auto flex flex-col py-16", className)}>
<header className="flex flex-col items-center justify-between"> <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"> <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} {title}
</div> </div>
{subtitle && ( {subtitle && (
<div className="text-muted-foreground text-center text-xl"> <div className="text-muted-foreground text-center text-xl">
{subtitle} {subtitle}
</div> </div>
)} )}
</header> </header>
<main className="mt-4">{children}</main> <main className="mt-4">{children}</main>
</section> </section>
); );
} }
@@ -1,99 +1,99 @@
import Link from "next/link"; import Link from "next/link";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { pathOfThread } from "@/core/threads/utils"; import { pathOfThread } from "@/core/threads/utils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Section } from "../section"; import { Section } from "../section";
export function CaseStudySection({ className }: { className?: string }) { export function CaseStudySection({ className }: { className?: string }) {
const caseStudies = [ const caseStudies = [
{ {
threadId: "7cfa5f8f-a2f8-47ad-acbd-da7137baf990", threadId: "7cfa5f8f-a2f8-47ad-acbd-da7137baf990",
title: "Forecast 2026 Agent Trends and Opportunities", title: "Forecast 2026 Agent Trends and Opportunities",
description: description:
"Create a webpage with a Deep Research report forecasting the agent technology trends and opportunities in 2026.", "Create a webpage with a Deep Research report forecasting the agent technology trends and opportunities in 2026.",
}, },
{ {
threadId: "4f3e55ee-f853-43db-bfb3-7d1a411f03cb", threadId: "4f3e55ee-f853-43db-bfb3-7d1a411f03cb",
title: 'Generate a Video Based On the Novel "Pride and Prejudice"', title: 'Generate a Video Based On the Novel "Pride and Prejudice"',
description: 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.', '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", threadId: "21cfea46-34bd-4aa6-9e1f-3009452fbeb9",
title: "Doraemon Explains the MOE Architecture", title: "Doraemon Explains the MOE Architecture",
description: description:
"Generate a Doraemon comic strip explaining the MOE architecture to the teenagers who are interested in AI.", "Generate a Doraemon comic strip explaining the MOE architecture to the teenagers who are interested in AI.",
}, },
{ {
threadId: "ad76c455-5bf9-4335-8517-fc03834ab828", threadId: "ad76c455-5bf9-4335-8517-fc03834ab828",
title: "An Exploratory Data Analysis of the Titanic Dataset", title: "An Exploratory Data Analysis of the Titanic Dataset",
description: description:
"Explore the Titanic dataset and identify the key factors that influenced survival rates with visualizations and insights.", "Explore the Titanic dataset and identify the key factors that influenced survival rates with visualizations and insights.",
}, },
{ {
threadId: "d3e5adaf-084c-4dd5-9d29-94f1d6bccd98", threadId: "d3e5adaf-084c-4dd5-9d29-94f1d6bccd98",
title: "Watch Y Combinator's Video then Conduct a Deep Research", title: "Watch Y Combinator's Video then Conduct a Deep Research",
description: description:
"Watch the given Y Combinator's YouTube video and conduct a deep research on the YC's tips for technical startup founders.", "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", threadId: "3823e443-4e2b-4679-b496-a9506eae462b",
title: "Collect and Summarize Dr. Fei Fei Li's Podcasts", title: "Collect and Summarize Dr. Fei Fei Li's Podcasts",
description: description:
"Collect all the podcast appearances of Dr. Fei Fei Li in the last 6 months, then summarize them into a comprehensive report.", "Collect all the podcast appearances of Dr. Fei Fei Li in the last 6 months, then summarize them into a comprehensive report.",
}, },
]; ];
return ( return (
<Section <Section
className={className} className={className}
title="Case Studies" title="Case Studies"
subtitle="CM 助手" 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"> <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) => ( {caseStudies.map((caseStudy) => (
<Link <Link
key={caseStudy.title} key={caseStudy.title}
href={pathOfThread(caseStudy.threadId) + "?mock=true"} href={pathOfThread(caseStudy.threadId) + "?mock=true"}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Card className="group/card relative h-64 overflow-hidden"> <Card className="group/card relative h-64 overflow-hidden">
<div <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" 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={{ style={{
backgroundImage: `url(/images/${caseStudy.threadId}.jpg)`, backgroundImage: `url(/images/${caseStudy.threadId}.jpg)`,
}} }}
></div> ></div>
<div <div
className={cn( className={cn(
"flex h-full w-full translate-y-[calc(100%-60px)] flex-col items-center", "flex h-full w-full translate-y-[calc(100%-60px)] flex-col items-center",
"transition-all duration-300", "transition-all duration-300",
"group-hover/card:translate-y-[calc(100%-128px)]", "group-hover/card:translate-y-[calc(100%-128px)]",
)} )}
> >
<div <div
className="flex w-full flex-col p-4" className="flex w-full flex-col p-4"
style={{ style={{
background: background:
"linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%)", "linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%)",
}} }}
> >
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h3 className="flex h-14 items-center text-xl font-bold text-shadow-black"> <h3 className="flex h-14 items-center text-xl font-bold text-shadow-black">
{caseStudy.title} {caseStudy.title}
</h3> </h3>
<p className="box-shadow-black overflow-hidden text-sm text-white/85 text-shadow-black"> <p className="box-shadow-black overflow-hidden text-sm text-white/85 text-shadow-black">
{caseStudy.description} {caseStudy.description}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</Card> </Card>
</Link> </Link>
))} ))}
</div> </div>
</Section> </Section>
); );
} }
@@ -1,20 +1,20 @@
"use client"; "use client";
import { AuroraText } from "@/components/ui/aurora-text"; import { AuroraText } from "@/components/ui/aurora-text";
import { Section } from "../section"; import { Section } from "../section";
export function CommunitySection() { export function CommunitySection() {
return ( return (
<Section <Section
title={ title={
<AuroraText colors={["#60A5FA", "#A5FA60", "#A560FA"]}> <AuroraText colors={["#60A5FA", "#A5FA60", "#A560FA"]}>
Join the Community Join the Community
</AuroraText> </AuroraText>
} }
subtitle="Contribute brilliant ideas to shape the future of CM Assistant. Collaborate, innovate, and make impacts." subtitle="Contribute brilliant ideas to shape the future of CM Assistant. Collaborate, innovate, and make impacts."
> >
<div /> <div />
</Section> </Section>
); );
} }
@@ -1,126 +1,126 @@
"use client"; "use client";
import { import {
AnimatedSpan, AnimatedSpan,
Terminal, Terminal,
TypingAnimation, TypingAnimation,
} from "@/components/ui/terminal"; } from "@/components/ui/terminal";
import { Section } from "../section"; import { Section } from "../section";
export function SandboxSection({ className }: { className?: string }) { export function SandboxSection({ className }: { className?: string }) {
return ( return (
<Section <Section
className={className} className={className}
title="Agent Runtime Environment" title="Agent Runtime Environment"
subtitle={ subtitle={
<p> <p>
We give CM Assistant a &quot;computer&quot;, which can execute commands, We give CM Assistant a &quot;computer&quot;, which can execute commands,
manage files, and run long tasks — all in a secure Docker-based manage files, and run long tasks — all in a secure Docker-based
sandbox sandbox
</p> </p>
} }
> >
<div className="mt-8 flex w-full max-w-6xl flex-col items-center gap-12 lg:flex-row lg:gap-16"> <div className="mt-8 flex w-full max-w-6xl flex-col items-center gap-12 lg:flex-row lg:gap-16">
{/* Left: Terminal */} {/* Left: Terminal */}
<div className="w-full flex-1"> <div className="w-full flex-1">
<Terminal className="h-[360px] w-full"> <Terminal className="h-[360px] w-full">
{/* Scene 1: Build a Game */} {/* Scene 1: Build a Game */}
<TypingAnimation>$ cat requirements.txt</TypingAnimation> <TypingAnimation>$ cat requirements.txt</TypingAnimation>
<AnimatedSpan delay={800} className="text-zinc-400"> <AnimatedSpan delay={800} className="text-zinc-400">
pygame==2.5.0 pygame==2.5.0
</AnimatedSpan> </AnimatedSpan>
<TypingAnimation delay={1200}> <TypingAnimation delay={1200}>
$ pip install -r requirements.txt $ pip install -r requirements.txt
</TypingAnimation> </TypingAnimation>
<AnimatedSpan delay={2000} className="text-green-500"> <AnimatedSpan delay={2000} className="text-green-500">
[ok] Installed pygame [ok] Installed pygame
</AnimatedSpan> </AnimatedSpan>
<TypingAnimation delay={2400}> <TypingAnimation delay={2400}>
$ write game.py --lines 156 $ write game.py --lines 156
</TypingAnimation> </TypingAnimation>
<AnimatedSpan delay={3200} className="text-blue-500"> <AnimatedSpan delay={3200} className="text-blue-500">
[ok] Written 156 lines [ok] Written 156 lines
</AnimatedSpan> </AnimatedSpan>
<TypingAnimation delay={3600}> <TypingAnimation delay={3600}>
$ python game.py --test $ python game.py --test
</TypingAnimation> </TypingAnimation>
<AnimatedSpan delay={4200} className="text-green-500"> <AnimatedSpan delay={4200} className="text-green-500">
[ok] All sprites loaded [ok] All sprites loaded
</AnimatedSpan> </AnimatedSpan>
<AnimatedSpan delay={4500} className="text-green-500"> <AnimatedSpan delay={4500} className="text-green-500">
[ok] Physics engine OK [ok] Physics engine OK
</AnimatedSpan> </AnimatedSpan>
<AnimatedSpan delay={4800} className="text-green-500"> <AnimatedSpan delay={4800} className="text-green-500">
[ok] 60 FPS stable [ok] 60 FPS stable
</AnimatedSpan> </AnimatedSpan>
{/* Scene 2: Data Analysis */} {/* Scene 2: Data Analysis */}
<TypingAnimation delay={5400}> <TypingAnimation delay={5400}>
$ curl -O sales-2024.csv $ curl -O sales-2024.csv
</TypingAnimation> </TypingAnimation>
<AnimatedSpan delay={6200} className="text-zinc-400"> <AnimatedSpan delay={6200} className="text-zinc-400">
Downloaded 12.4 MB Downloaded 12.4 MB
</AnimatedSpan> </AnimatedSpan>
</Terminal> </Terminal>
</div> </div>
{/* Right: Description */} {/* Right: Description */}
<div className="w-full flex-1 space-y-6"> <div className="w-full flex-1 space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm font-medium tracking-wider text-purple-400 uppercase"> <p className="text-sm font-medium tracking-wider text-purple-400 uppercase">
Open-source Open-source
</p> </p>
<h2 className="text-4xl font-bold tracking-tight lg:text-5xl"> <h2 className="text-4xl font-bold tracking-tight lg:text-5xl">
<a <a
href="https://github.com/agent-infra/sandbox" href="https://github.com/agent-infra/sandbox"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
AIO Sandbox AIO Sandbox
</a> </a>
</h2> </h2>
</div> </div>
<div className="space-y-4 text-lg text-zinc-400"> <div className="space-y-4 text-lg text-zinc-400">
<p> <p>
We recommend using{" "} We recommend using{" "}
<a <a
href="https://github.com/agent-infra/sandbox" href="https://github.com/agent-infra/sandbox"
className="underline" className="underline"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
All-in-One Sandbox All-in-One Sandbox
</a>{" "} </a>{" "}
that combines Browser, Shell, File, MCP and VSCode Server in a that combines Browser, Shell, File, MCP and VSCode Server in a
single Docker container. single Docker container.
</p> </p>
</div> </div>
{/* Feature Tags */} {/* Feature Tags */}
<div className="flex flex-wrap gap-3 pt-4"> <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"> <span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Isolated Isolated
</span> </span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300"> <span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Safe Safe
</span> </span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300"> <span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Persistent Persistent
</span> </span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300"> <span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Mountable FS Mountable FS
</span> </span>
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300"> <span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
Long-running Long-running
</span> </span>
</div> </div>
</div> </div>
</div> </div>
</Section> </Section>
); );
} }
@@ -1,28 +1,28 @@
"use client"; "use client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import ProgressiveSkillsAnimation from "../progressive-skills-animation"; import ProgressiveSkillsAnimation from "../progressive-skills-animation";
import { Section } from "../section"; import { Section } from "../section";
export function SkillsSection({ className }: { className?: string }) { export function SkillsSection({ className }: { className?: string }) {
return ( return (
<Section <Section
className={cn("h-[calc(100vh-64px)] w-full bg-white/2", className)} className={cn("h-[calc(100vh-64px)] w-full bg-white/2", className)}
title="Agent Skills" title="Agent Skills"
subtitle={ subtitle={
<div> <div>
Agent Skills are loaded progressively — only what&apos;s needed, when Agent Skills are loaded progressively — only what&apos;s needed, when
it&apos;s needed. it&apos;s needed.
<br /> <br />
Extend CM Assistant with your own skill files, or use our built-in Extend CM Assistant with your own skill files, or use our built-in
library. library.
</div> </div>
} }
> >
<div className="relative overflow-hidden"> <div className="relative overflow-hidden">
<ProgressiveSkillsAnimation /> <ProgressiveSkillsAnimation />
</div> </div>
</Section> </Section>
); );
} }
@@ -1,63 +1,63 @@
"use client"; "use client";
import MagicBento, { type BentoCardProps } from "@/components/ui/magic-bento"; import MagicBento, { type BentoCardProps } from "@/components/ui/magic-bento";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Section } from "../section"; import { Section } from "../section";
const COLOR = "#0a0a0a"; const COLOR = "#0a0a0a";
const features: BentoCardProps[] = [ const features: BentoCardProps[] = [
{ {
color: COLOR, color: COLOR,
label: "Context Engineering", label: "Context Engineering",
title: "Long/Short-term Memory", title: "Long/Short-term Memory",
description: "Now the agent can better understand you", description: "Now the agent can better understand you",
}, },
{ {
color: COLOR, color: COLOR,
label: "Long Task Running", label: "Long Task Running",
title: "Planning and Sub-tasking", title: "Planning and Sub-tasking",
description: description:
"Plans ahead, reasons through complexity, then executes sequentially or in parallel", "Plans ahead, reasons through complexity, then executes sequentially or in parallel",
}, },
{ {
color: COLOR, color: COLOR,
label: "Extensible", label: "Extensible",
title: "Skills and Tools", title: "Skills and Tools",
description: description:
"Plug, play, or even swap built-in tools. Build the agent you want.", "Plug, play, or even swap built-in tools. Build the agent you want.",
}, },
{ {
color: COLOR, color: COLOR,
label: "Persistent", label: "Persistent",
title: "Sandbox with File System", title: "Sandbox with File System",
description: "Read, write, run — like a real computer", description: "Read, write, run — like a real computer",
}, },
{ {
color: COLOR, color: COLOR,
label: "Flexible", label: "Flexible",
title: "Multi-Model Support", title: "Multi-Model Support",
description: "Doubao, DeepSeek, OpenAI, Gemini, etc.", description: "Doubao, DeepSeek, OpenAI, Gemini, etc.",
}, },
{ {
color: COLOR, color: COLOR,
label: "Free", label: "Free",
title: "Open Source", title: "Open Source",
description: "MIT License, self-hosted, full control", description: "MIT License, self-hosted, full control",
}, },
]; ];
export function WhatsNewSection({ className }: { className?: string }) { export function WhatsNewSection({ className }: { className?: string }) {
return ( return (
<Section <Section
className={cn("", className)} className={cn("", className)}
title="CM 助手" title="CM 助手"
subtitle="CM 助手" subtitle="CM 助手"
> >
<div className="flex w-full items-center justify-center"> <div className="flex w-full items-center justify-center">
<MagicBento data={features} /> <MagicBento data={features} />
</div> </div>
</Section> </Section>
); );
} }
@@ -1,20 +1,20 @@
"use client"; "use client";
import { import {
QueryClient, QueryClient,
QueryClientProvider as TanStackQueryClientProvider, QueryClientProvider as TanStackQueryClientProvider,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
export const queryClient = new QueryClient(); export const queryClient = new QueryClient();
export function QueryClientProvider({ export function QueryClientProvider({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<TanStackQueryClientProvider client={queryClient}> <TanStackQueryClientProvider client={queryClient}>
{children} {children}
</TanStackQueryClientProvider> </TanStackQueryClientProvider>
); );
} }
+19 -19
View File
@@ -1,19 +1,19 @@
"use client"; "use client";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { ThemeProvider as NextThemesProvider } from "next-themes"; import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({ export function ThemeProvider({
children, children,
...props ...props
}: React.ComponentProps<typeof NextThemesProvider>) { }: React.ComponentProps<typeof NextThemesProvider>) {
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<NextThemesProvider <NextThemesProvider
{...props} {...props}
forcedTheme={pathname === "/landing" ? "dark" : undefined} forcedTheme={pathname === "/landing" ? "dark" : undefined}
> >
{children} {children}
</NextThemesProvider> </NextThemesProvider>
); );
} }
+66 -66
View File
@@ -1,66 +1,66 @@
import * as React from "react"; import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const alertVariants = cva( 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", "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: { variants: {
variant: { variant: {
default: "bg-card text-card-foreground", default: "bg-card text-card-foreground",
destructive: destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
}, },
); );
function Alert({ function Alert({
className, className,
variant, variant,
...props ...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) { }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return ( return (
<div <div
data-slot="alert" data-slot="alert"
role="alert" role="alert"
className={cn(alertVariants({ variant }), className)} className={cn(alertVariants({ variant }), className)}
{...props} {...props}
/> />
); );
} }
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="alert-title" data-slot="alert-title"
className={cn( className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function AlertDescription({ function AlertDescription({
className, className,
...props ...props
}: React.ComponentProps<"div">) { }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="alert-description" data-slot="alert-description"
className={cn( className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed", "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
export { Alert, AlertTitle, AlertDescription }; export { Alert, AlertTitle, AlertDescription };
+43 -43
View File
@@ -1,43 +1,43 @@
"use client"; "use client";
import React, { memo } from "react"; import React, { memo } from "react";
interface AuroraTextProps { interface AuroraTextProps {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
colors?: string[]; colors?: string[];
speed?: number; speed?: number;
} }
export const AuroraText = memo( export const AuroraText = memo(
({ ({
children, children,
className = "", className = "",
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"], colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
speed = 1, speed = 1,
}: AuroraTextProps) => { }: AuroraTextProps) => {
const gradientStyle = { const gradientStyle = {
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${ backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
colors[0] colors[0]
})`, })`,
WebkitBackgroundClip: "text", WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent", WebkitTextFillColor: "transparent",
animationDuration: `${10 / speed}s`, animationDuration: `${10 / speed}s`,
}; };
return ( return (
<span className={`relative inline-block ${className}`}> <span className={`relative inline-block ${className}`}>
<span className="sr-only">{children}</span> <span className="sr-only">{children}</span>
<span <span
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent" className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
style={gradientStyle} style={gradientStyle}
aria-hidden="true" aria-hidden="true"
> >
{children} {children}
</span> </span>
</span> </span>
); );
}, },
); );
AuroraText.displayName = "AuroraText"; AuroraText.displayName = "AuroraText";
+53 -53
View File
@@ -1,53 +1,53 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar"; import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Avatar({ function Avatar({
className, className,
...props ...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) { }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return ( return (
<AvatarPrimitive.Root <AvatarPrimitive.Root
data-slot="avatar" data-slot="avatar"
className={cn( className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full", "relative flex size-8 shrink-0 overflow-hidden rounded-full",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function AvatarImage({ function AvatarImage({
className, className,
...props ...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) { }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return ( return (
<AvatarPrimitive.Image <AvatarPrimitive.Image
data-slot="avatar-image" data-slot="avatar-image"
className={cn("aspect-square size-full", className)} className={cn("aspect-square size-full", className)}
{...props} {...props}
/> />
); );
} }
function AvatarFallback({ function AvatarFallback({
className, className,
...props ...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return ( return (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
data-slot="avatar-fallback" data-slot="avatar-fallback"
className={cn( className={cn(
"bg-muted flex size-full items-center justify-center rounded-full", "bg-muted flex size-full items-center justify-center rounded-full",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
export { Avatar, AvatarImage, AvatarFallback }; export { Avatar, AvatarImage, AvatarFallback };
+46 -46
View File
@@ -1,46 +1,46 @@
import * as React from "react"; import * as React from "react";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const badgeVariants = cva( 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", "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: { variants: {
variant: { variant: {
default: default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive: 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", "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: outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
}, },
); );
function Badge({ function Badge({
className, className,
variant, variant,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"span"> & }: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) { VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"; const Comp = asChild ? Slot : "span";
return ( return (
<Comp <Comp
data-slot="badge" data-slot="badge"
className={cn(badgeVariants({ variant }), className)} className={cn(badgeVariants({ variant }), className)}
{...props} {...props}
/> />
); );
} }
export { Badge, badgeVariants }; export { Badge, badgeVariants };
+92 -92
View File
@@ -1,36 +1,36 @@
import * as React from "react"; import * as React from "react";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react"; import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />; return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
} }
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) { function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return ( return (
<ol <ol
data-slot="breadcrumb-list" data-slot="breadcrumb-list"
className={cn( className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5", "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) { function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return ( return (
<li <li
data-slot="breadcrumb-item" data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)} className={cn("inline-flex items-center gap-1.5", className)}
{...props} {...props}
/> />
); );
} }
const BreadcrumbLink = React.forwardRef< const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement, HTMLAnchorElement,
React.ComponentProps<"a"> & { React.ComponentProps<"a"> & {
@@ -50,62 +50,62 @@ const BreadcrumbLink = React.forwardRef<
}); });
BreadcrumbLink.displayName = "BreadcrumbLink"; BreadcrumbLink.displayName = "BreadcrumbLink";
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) { function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="breadcrumb-page" data-slot="breadcrumb-page"
role="link" role="link"
aria-disabled="true" aria-disabled="true"
aria-current="page" aria-current="page"
className={cn("text-foreground font-normal", className)} className={cn("text-foreground font-normal", className)}
{...props} {...props}
/> />
); );
} }
function BreadcrumbSeparator({ function BreadcrumbSeparator({
children, children,
className, className,
...props ...props
}: React.ComponentProps<"li">) { }: React.ComponentProps<"li">) {
return ( return (
<li <li
data-slot="breadcrumb-separator" data-slot="breadcrumb-separator"
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)} className={cn("[&>svg]:size-3.5", className)}
{...props} {...props}
> >
{children ?? <ChevronRight />} {children ?? <ChevronRight />}
</li> </li>
); );
} }
function BreadcrumbEllipsis({ function BreadcrumbEllipsis({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="breadcrumb-ellipsis" data-slot="breadcrumb-ellipsis"
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)} className={cn("flex size-9 items-center justify-center", className)}
{...props} {...props}
> >
<MoreHorizontal className="size-4" /> <MoreHorizontal className="size-4" />
<span className="sr-only">More</span> <span className="sr-only">More</span>
</span> </span>
); );
} }
export { export {
Breadcrumb, Breadcrumb,
BreadcrumbList, BreadcrumbList,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
BreadcrumbEllipsis, BreadcrumbEllipsis,
}; };
+83 -83
View File
@@ -1,83 +1,83 @@
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
const buttonGroupVariants = cva( 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", "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: { variants: {
orientation: { orientation: {
horizontal: horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
vertical: vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none", "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
}, },
}, },
defaultVariants: { defaultVariants: {
orientation: "horizontal", orientation: "horizontal",
}, },
}, },
); );
function ButtonGroup({ function ButtonGroup({
className, className,
orientation, orientation,
...props ...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) { }: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return ( return (
<div <div
role="group" role="group"
data-slot="button-group" data-slot="button-group"
data-orientation={orientation} data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)} className={cn(buttonGroupVariants({ orientation }), className)}
{...props} {...props}
/> />
); );
} }
function ButtonGroupText({ function ButtonGroupText({
className, className,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
asChild?: boolean; asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot : "div"; const Comp = asChild ? Slot : "div";
return ( return (
<Comp <Comp
className={cn( 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", "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, className,
)} )}
{...props} {...props}
/> />
); );
} }
function ButtonGroupSeparator({ function ButtonGroupSeparator({
className, className,
orientation = "vertical", orientation = "vertical",
...props ...props
}: React.ComponentProps<typeof Separator>) { }: React.ComponentProps<typeof Separator>) {
return ( return (
<Separator <Separator
data-slot="button-group-separator" data-slot="button-group-separator"
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto", "bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
export { export {
ButtonGroup, ButtonGroup,
ButtonGroupSeparator, ButtonGroupSeparator,
ButtonGroupText, ButtonGroupText,
buttonGroupVariants, buttonGroupVariants,
}; };
+62 -62
View File
@@ -1,63 +1,63 @@
import * as React from "react"; import * as React from "react";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const buttonVariants = cva( 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", "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: { variants: {
variant: { variant: {
default: default:
"cursor-pointer bg-primary text-[var(--button-foreground,var(--primary-foreground))] hover:bg-primary/90", "cursor-pointer bg-primary text-[var(--button-foreground,var(--primary-foreground))] hover:bg-primary/90",
destructive: 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", "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: outline:
"cursor-pointer border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-[#213D5E] dark:bg-[#022248] dark:text-[#CCD2DA] dark:hover:border-[#213D5E] dark:hover:bg-[#0a2f58] dark:hover:text-[#CCD2DA] dark:[&_svg]:text-[#CCD2DA]", "cursor-pointer border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-[#213D5E] dark:bg-[#022248] dark:text-[#CCD2DA] dark:hover:border-[#213D5E] dark:hover:bg-[#0a2f58] dark:hover:text-[#CCD2DA] dark:[&_svg]:text-[#CCD2DA]",
secondary: secondary:
"cursor-pointer bg-secondary text-secondary-foreground hover:bg-secondary/80", "cursor-pointer bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: ghost:
"cursor-pointer hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "cursor-pointer hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "cursor-pointer text-primary underline-offset-4 hover:underline", link: "cursor-pointer text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", 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", 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", lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9", icon: "size-9",
"icon-sm": "size-8", "icon-sm": "size-8",
"icon-lg": "size-10", "icon-lg": "size-10",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default", size: "default",
}, },
}, },
); );
const Button = React.forwardRef< const Button = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
React.ComponentProps<"button"> & React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean; asChild?: boolean;
} }
>(({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => { >(({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"; const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
ref={ref} ref={ref}
data-slot="button" data-slot="button"
{...(variant !== undefined && { "data-variant": variant })} {...(variant !== undefined && { "data-variant": variant })}
{...(size !== undefined && { "data-size": size })} {...(size !== undefined && { "data-size": size })}
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />
); );
}); });
Button.displayName = "Button"; Button.displayName = "Button";
export { Button, buttonVariants }; export { Button, buttonVariants };
+92 -92
View File
@@ -1,92 +1,92 @@
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) { function Card({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-[4px] border py-6 shadow-sm dark:border-[#0049A5] dark:bg-[#02244C]", "bg-card text-card-foreground flex flex-col gap-6 rounded-[4px] border py-6 shadow-sm dark:border-[#0049A5] dark:bg-[#02244C]",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function CardHeader({ className, ...props }: React.ComponentProps<"div">) { function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-header" data-slot="card-header"
className={cn( 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", "@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, className,
)} )}
{...props} {...props}
/> />
); );
} }
function CardTitle({ className, ...props }: React.ComponentProps<"div">) { function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-title" data-slot="card-title"
className={cn("leading-none font-semibold", className)} className={cn("leading-none font-semibold", className)}
{...props} {...props}
/> />
); );
} }
function CardDescription({ className, ...props }: React.ComponentProps<"div">) { function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-description" data-slot="card-description"
className={cn("text-muted-foreground text-sm dark:text-[#F2F3F5]", className)} className={cn("text-muted-foreground text-sm dark:text-[#F2F3F5]", className)}
{...props} {...props}
/> />
); );
} }
function CardAction({ className, ...props }: React.ComponentProps<"div">) { function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-action" data-slot="card-action"
className={cn( className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end", "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function CardContent({ className, ...props }: React.ComponentProps<"div">) { function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-content" data-slot="card-content"
className={cn("px-6 ", className)} className={cn("px-6 ", className)}
{...props} {...props}
/> />
); );
} }
function CardFooter({ className, ...props }: React.ComponentProps<"div">) { function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-footer" data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)} className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props} {...props}
/> />
); );
} }
export { export {
Card, Card,
CardHeader, CardHeader,
CardFooter, CardFooter,
CardTitle, CardTitle,
CardAction, CardAction,
CardDescription, CardDescription,
CardContent, CardContent,
}; };
+241 -241
View File
@@ -1,241 +1,241 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import useEmblaCarousel, { import useEmblaCarousel, {
type UseEmblaCarouselType, type UseEmblaCarouselType,
} from "embla-carousel-react"; } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react"; import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1]; type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>; type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0]; type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1]; type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = { type CarouselProps = {
opts?: CarouselOptions; opts?: CarouselOptions;
plugins?: CarouselPlugin; plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical"; orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void; setApi?: (api: CarouselApi) => void;
}; };
type CarouselContextProps = { type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]; carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1]; api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void; scrollPrev: () => void;
scrollNext: () => void; scrollNext: () => void;
canScrollPrev: boolean; canScrollPrev: boolean;
canScrollNext: boolean; canScrollNext: boolean;
} & CarouselProps; } & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null); const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() { function useCarousel() {
const context = React.useContext(CarouselContext); const context = React.useContext(CarouselContext);
if (!context) { if (!context) {
throw new Error("useCarousel must be used within a <Carousel />"); throw new Error("useCarousel must be used within a <Carousel />");
} }
return context; return context;
} }
function Carousel({ function Carousel({
orientation = "horizontal", orientation = "horizontal",
opts, opts,
setApi, setApi,
plugins, plugins,
className, className,
children, children,
...props ...props
}: React.ComponentProps<"div"> & CarouselProps) { }: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel( const [carouselRef, api] = useEmblaCarousel(
{ {
...opts, ...opts,
axis: orientation === "horizontal" ? "x" : "y", axis: orientation === "horizontal" ? "x" : "y",
}, },
plugins, plugins,
); );
const [canScrollPrev, setCanScrollPrev] = React.useState(false); const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false); const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => { const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return; if (!api) return;
setCanScrollPrev(api.canScrollPrev()); setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext()); setCanScrollNext(api.canScrollNext());
}, []); }, []);
const scrollPrev = React.useCallback(() => { const scrollPrev = React.useCallback(() => {
api?.scrollPrev(); api?.scrollPrev();
}, [api]); }, [api]);
const scrollNext = React.useCallback(() => { const scrollNext = React.useCallback(() => {
api?.scrollNext(); api?.scrollNext();
}, [api]); }, [api]);
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") { if (event.key === "ArrowLeft") {
event.preventDefault(); event.preventDefault();
scrollPrev(); scrollPrev();
} else if (event.key === "ArrowRight") { } else if (event.key === "ArrowRight") {
event.preventDefault(); event.preventDefault();
scrollNext(); scrollNext();
} }
}, },
[scrollPrev, scrollNext], [scrollPrev, scrollNext],
); );
React.useEffect(() => { React.useEffect(() => {
if (!api || !setApi) return; if (!api || !setApi) return;
setApi(api); setApi(api);
}, [api, setApi]); }, [api, setApi]);
React.useEffect(() => { React.useEffect(() => {
if (!api) return; if (!api) return;
onSelect(api); onSelect(api);
api.on("reInit", onSelect); api.on("reInit", onSelect);
api.on("select", onSelect); api.on("select", onSelect);
return () => { return () => {
api?.off("select", onSelect); api?.off("select", onSelect);
}; };
}, [api, onSelect]); }, [api, onSelect]);
return ( return (
<CarouselContext.Provider <CarouselContext.Provider
value={{ value={{
carouselRef, carouselRef,
api: api, api: api,
opts, opts,
orientation: orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev, scrollPrev,
scrollNext, scrollNext,
canScrollPrev, canScrollPrev,
canScrollNext, canScrollNext,
}} }}
> >
<div <div
onKeyDownCapture={handleKeyDown} onKeyDownCapture={handleKeyDown}
className={cn("relative", className)} className={cn("relative", className)}
role="region" role="region"
aria-roledescription="carousel" aria-roledescription="carousel"
data-slot="carousel" data-slot="carousel"
{...props} {...props}
> >
{children} {children}
</div> </div>
</CarouselContext.Provider> </CarouselContext.Provider>
); );
} }
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel(); const { carouselRef, orientation } = useCarousel();
return ( return (
<div <div
ref={carouselRef} ref={carouselRef}
className="overflow-hidden" className="overflow-hidden"
data-slot="carousel-content" data-slot="carousel-content"
> >
<div <div
className={cn( className={cn(
"flex", "flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className, className,
)} )}
{...props} {...props}
/> />
</div> </div>
); );
} }
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel(); const { orientation } = useCarousel();
return ( return (
<div <div
role="group" role="group"
aria-roledescription="slide" aria-roledescription="slide"
data-slot="carousel-item" data-slot="carousel-item"
className={cn( className={cn(
"min-w-0 shrink-0 grow-0 basis-full", "min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4", orientation === "horizontal" ? "pl-4" : "pt-4",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function CarouselPrevious({ function CarouselPrevious({
className, className,
variant = "outline", variant = "outline",
size = "icon", size = "icon",
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel(); const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return ( return (
<Button <Button
data-slot="carousel-previous" data-slot="carousel-previous"
variant={variant} variant={variant}
size={size} size={size}
className={cn( className={cn(
"absolute size-8 rounded-full", "absolute size-8 rounded-full",
orientation === "horizontal" orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2" ? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className, className,
)} )}
disabled={!canScrollPrev} disabled={!canScrollPrev}
onClick={scrollPrev} onClick={scrollPrev}
{...props} {...props}
> >
<ArrowLeft /> <ArrowLeft />
<span className="sr-only">Previous slide</span> <span className="sr-only">Previous slide</span>
</Button> </Button>
); );
} }
function CarouselNext({ function CarouselNext({
className, className,
variant = "outline", variant = "outline",
size = "icon", size = "icon",
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel(); const { orientation, scrollNext, canScrollNext } = useCarousel();
return ( return (
<Button <Button
data-slot="carousel-next" data-slot="carousel-next"
variant={variant} variant={variant}
size={size} size={size}
className={cn( className={cn(
"absolute size-8 rounded-full", "absolute size-8 rounded-full",
orientation === "horizontal" orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2" ? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className, className,
)} )}
disabled={!canScrollNext} disabled={!canScrollNext}
onClick={scrollNext} onClick={scrollNext}
{...props} {...props}
> >
<ArrowRight /> <ArrowRight />
<span className="sr-only">Next slide</span> <span className="sr-only">Next slide</span>
</Button> </Button>
); );
} }
export { export {
type CarouselApi, type CarouselApi,
Carousel, Carousel,
CarouselContent, CarouselContent,
CarouselItem, CarouselItem,
CarouselPrevious, CarouselPrevious,
CarouselNext, CarouselNext,
}; };
+11 -11
View File
@@ -2,15 +2,15 @@
import * as React from "react"; import * as React from "react";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Collapsible({ function Collapsible({
...props ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />; return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
} }
const CollapsibleTrigger = React.forwardRef< const CollapsibleTrigger = React.forwardRef<
React.ElementRef<typeof CollapsiblePrimitive.CollapsibleTrigger>, React.ElementRef<typeof CollapsiblePrimitive.CollapsibleTrigger>,
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleTrigger> React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleTrigger>
@@ -43,5 +43,5 @@ const CollapsibleContent = React.forwardRef<
CollapsibleContent.displayName = CollapsibleContent.displayName =
CollapsiblePrimitive.CollapsibleContent.displayName; CollapsiblePrimitive.CollapsibleContent.displayName;
export { Collapsible, CollapsibleTrigger, CollapsibleContent }; export { Collapsible, CollapsibleTrigger, CollapsibleContent };
+181 -181
View File
@@ -1,181 +1,181 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { Command as CommandPrimitive } from "cmdk"; import { Command as CommandPrimitive } from "cmdk";
import { SearchIcon } from "lucide-react"; import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
function Command({ function Command({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive>) { }: React.ComponentProps<typeof CommandPrimitive>) {
return ( return (
<CommandPrimitive <CommandPrimitive
data-slot="command" data-slot="command"
className={cn( className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md", "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function CommandDialog({ function CommandDialog({
title = "Command Palette", title = "Command Palette",
description = "Search for a command to run...", description = "Search for a command to run...",
children, children,
className, className,
showCloseButton = true, showCloseButton = true,
...props ...props
}: React.ComponentProps<typeof Dialog> & { }: React.ComponentProps<typeof Dialog> & {
title?: string; title?: string;
description?: string; description?: string;
className?: string; className?: string;
showCloseButton?: boolean; showCloseButton?: boolean;
}) { }) {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogContent <DialogContent
className={cn("overflow-hidden p-0", className)} className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton} showCloseButton={showCloseButton}
> >
<DialogTitle className="sr-only">{title}</DialogTitle> <DialogTitle className="sr-only">{title}</DialogTitle>
<DialogDescription className="sr-only">{description}</DialogDescription> <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"> <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} {children}
</Command> </Command>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
} }
function CommandInput({ function CommandInput({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) { }: React.ComponentProps<typeof CommandPrimitive.Input>) {
return ( return (
<div <div
data-slot="command-input-wrapper" data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3" className="flex h-9 items-center gap-2 border-b px-3"
> >
<SearchIcon className="size-4 shrink-0 opacity-50" /> <SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input <CommandPrimitive.Input
data-slot="command-input" data-slot="command-input"
className={cn( 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", "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, className,
)} )}
{...props} {...props}
/> />
</div> </div>
); );
} }
function CommandList({ function CommandList({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.List>) { }: React.ComponentProps<typeof CommandPrimitive.List>) {
return ( return (
<CommandPrimitive.List <CommandPrimitive.List
data-slot="command-list" data-slot="command-list"
className={cn( className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function CommandEmpty({ function CommandEmpty({
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) { }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return ( return (
<CommandPrimitive.Empty <CommandPrimitive.Empty
data-slot="command-empty" data-slot="command-empty"
className="py-6 text-center text-sm" className="py-6 text-center text-sm"
{...props} {...props}
/> />
); );
} }
function CommandGroup({ function CommandGroup({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) { }: React.ComponentProps<typeof CommandPrimitive.Group>) {
return ( return (
<CommandPrimitive.Group <CommandPrimitive.Group
data-slot="command-group" data-slot="command-group"
className={cn( 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", "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, className,
)} )}
{...props} {...props}
/> />
); );
} }
function CommandSeparator({ function CommandSeparator({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) { }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return ( return (
<CommandPrimitive.Separator <CommandPrimitive.Separator
data-slot="command-separator" data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)} className={cn("bg-border -mx-1 h-px", className)}
{...props} {...props}
/> />
); );
} }
function CommandItem({ function CommandItem({
className, className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) { }: React.ComponentProps<typeof CommandPrimitive.Item>) {
return ( return (
<CommandPrimitive.Item <CommandPrimitive.Item
data-slot="command-item" data-slot="command-item"
className={cn( 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", "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, className,
)} )}
{...props} {...props}
/> />
); );
} }
function CommandShortcut({ function CommandShortcut({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="command-shortcut" data-slot="command-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
export { export {
Command, Command,
CommandDialog, CommandDialog,
CommandInput, CommandInput,
CommandList, CommandList,
CommandEmpty, CommandEmpty,
CommandGroup, CommandGroup,
CommandItem, CommandItem,
CommandShortcut, CommandShortcut,
CommandSeparator, CommandSeparator,
}; };
@@ -1,49 +1,49 @@
"use client"; "use client";
import React, { type MouseEventHandler } from "react"; import React, { type MouseEventHandler } from "react";
import confetti from "canvas-confetti"; import confetti from "canvas-confetti";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
interface ConfettiButtonProps extends React.ComponentProps<typeof Button> { interface ConfettiButtonProps extends React.ComponentProps<typeof Button> {
angle?: number; angle?: number;
particleCount?: number; particleCount?: number;
startVelocity?: number; startVelocity?: number;
spread?: number; spread?: number;
onClick?: MouseEventHandler<HTMLButtonElement>; onClick?: MouseEventHandler<HTMLButtonElement>;
} }
export function ConfettiButton({ export function ConfettiButton({
className, className,
children, children,
angle = 90, angle = 90,
particleCount = 75, particleCount = 75,
startVelocity = 35, startVelocity = 35,
spread = 70, spread = 70,
onClick, onClick,
...props ...props
}: ConfettiButtonProps) { }: ConfettiButtonProps) {
const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => { const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => {
const target = event.currentTarget; const target = event.currentTarget;
if (target) { if (target) {
const rect = target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
confetti({ confetti({
particleCount, particleCount,
startVelocity, startVelocity,
angle, angle,
spread, spread,
origin: { origin: {
x: (rect.left + rect.width / 2) / window.innerWidth, x: (rect.left + rect.width / 2) / window.innerWidth,
y: (rect.top + rect.height / 2) / window.innerHeight, y: (rect.top + rect.height / 2) / window.innerHeight,
}, },
}); });
} }
onClick?.(event); onClick?.(event);
}; };
return ( return (
<Button onClick={handleClick} className={className} {...props}> <Button onClick={handleClick} className={className} {...props}>
{children} {children}
</Button> </Button>
); );
} }
+160 -160
View File
@@ -1,76 +1,76 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog"; import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react"; import { XIcon } from "lucide-react";
import { isWujieEnv } from "@/core/embed/wujie"; import { isWujieEnv } from "@/core/embed/wujie";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Dialog({ function Dialog({
modal, modal,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) { }: React.ComponentProps<typeof DialogPrimitive.Root>) {
// 无界嵌入时 Radix modal 会给 body 注入异常 margin-right,导致右侧空白。 // 无界嵌入时 Radix modal 会给 body 注入异常 margin-right,导致右侧空白。
// 默认降为 non-modal;显式传 modal={true} 仍可强制 modal。 // 默认降为 non-modal;显式传 modal={true} 仍可强制 modal。
return ( return (
<DialogPrimitive.Root <DialogPrimitive.Root
data-slot="dialog" data-slot="dialog"
modal={modal ?? !isWujieEnv()} modal={modal ?? !isWujieEnv()}
{...props} {...props}
/> />
); );
} }
const DialogTrigger = React.forwardRef< const DialogTrigger = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Trigger>, React.ElementRef<typeof DialogPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
return ( return (
<DialogPrimitive.Trigger <DialogPrimitive.Trigger
ref={ref} ref={ref}
data-slot="dialog-trigger" data-slot="dialog-trigger"
{...props} {...props}
/> />
); );
}); });
DialogTrigger.displayName = DialogPrimitive.Trigger.displayName; DialogTrigger.displayName = DialogPrimitive.Trigger.displayName;
function DialogPortal({ function DialogPortal({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) { }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
} }
const DialogClose = React.forwardRef< const DialogClose = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Close>, React.ElementRef<typeof DialogPrimitive.Close>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
return <DialogPrimitive.Close ref={ref} data-slot="dialog-close" {...props} />; return <DialogPrimitive.Close ref={ref} data-slot="dialog-close" {...props} />;
}); });
DialogClose.displayName = DialogPrimitive.Close.displayName; DialogClose.displayName = DialogPrimitive.Close.displayName;
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
return ( return (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( 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", "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, className,
)} )}
{...props} {...props}
/> />
); );
}); });
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
@@ -82,90 +82,90 @@ const DialogContent = React.forwardRef<
return ( return (
<DialogPortal data-slot="dialog-portal"> <DialogPortal data-slot="dialog-portal">
<DialogOverlay className={overlayClassName} /> <DialogOverlay className={overlayClassName} />
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
data-slot="dialog-content" data-slot="dialog-content"
className={cn( 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", "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, className,
)} )}
{...props} {...props}
> >
{children} {children}
{showCloseButton && ( {showCloseButton && (
<DialogPrimitive.Close <DialogPrimitive.Close
data-slot="dialog-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" 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 /> <XIcon />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
)} )}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
); );
}); });
DialogContent.displayName = DialogPrimitive.Content.displayName; DialogContent.displayName = DialogPrimitive.Content.displayName;
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="dialog-header" data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)} className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props} {...props}
/> />
); );
} }
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="dialog-footer" data-slot="dialog-footer"
className={cn( className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function DialogTitle({ function DialogTitle({
className, className,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) { }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return ( return (
<DialogPrimitive.Title <DialogPrimitive.Title
data-slot="dialog-title" data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)} className={cn("text-lg leading-none font-semibold", className)}
{...props} {...props}
/> />
); );
} }
function DialogDescription({ function DialogDescription({
className, className,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) { }: React.ComponentProps<typeof DialogPrimitive.Description>) {
return ( return (
<DialogPrimitive.Description <DialogPrimitive.Description
data-slot="dialog-description" data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
); );
} }
export { export {
Dialog, Dialog,
DialogClose, DialogClose,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogOverlay, DialogOverlay,
DialogPortal, DialogPortal,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
}; };
+168 -168
View File
@@ -1,25 +1,25 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function DropdownMenu({ function DropdownMenu({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />; return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
} }
function DropdownMenuPortal({ function DropdownMenuPortal({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return ( return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
); );
} }
const DropdownMenuTrigger = React.forwardRef< const DropdownMenuTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>, React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
@@ -34,7 +34,7 @@ const DropdownMenuTrigger = React.forwardRef<
}); });
DropdownMenuTrigger.displayName = DropdownMenuPrimitive.Trigger.displayName; DropdownMenuTrigger.displayName = DropdownMenuPrimitive.Trigger.displayName;
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
@@ -56,15 +56,15 @@ const DropdownMenuContent = React.forwardRef<
}); });
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
function DropdownMenuGroup({ function DropdownMenuGroup({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return ( return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
); );
} }
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
@@ -88,123 +88,123 @@ const DropdownMenuItem = React.forwardRef<
}); });
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
function DropdownMenuCheckboxItem({ function DropdownMenuCheckboxItem({
className, className,
children, children,
checked, checked,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return ( return (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
className={cn( 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", "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, className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" /> <CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
); );
} }
function DropdownMenuRadioGroup({ function DropdownMenuRadioGroup({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return ( return (
<DropdownMenuPrimitive.RadioGroup <DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group" data-slot="dropdown-menu-radio-group"
{...props} {...props}
/> />
); );
} }
function DropdownMenuRadioItem({ function DropdownMenuRadioItem({
className, className,
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return ( return (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
className={cn( 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", "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, className,
)} )}
{...props} {...props}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" /> <CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
); );
} }
function DropdownMenuLabel({ function DropdownMenuLabel({
className, className,
inset, inset,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean; inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label" data-slot="dropdown-menu-label"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function DropdownMenuSeparator({ function DropdownMenuSeparator({
className, className,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return ( return (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator" data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
); );
} }
function DropdownMenuShortcut({ function DropdownMenuShortcut({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="dropdown-menu-shortcut" data-slot="dropdown-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function DropdownMenuSub({ function DropdownMenuSub({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />; return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
} }
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
@@ -230,7 +230,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
DropdownMenuSubTrigger.displayName = DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName; DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
@@ -250,21 +250,21 @@ const DropdownMenuSubContent = React.forwardRef<
DropdownMenuSubContent.displayName = DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName; DropdownMenuPrimitive.SubContent.displayName;
export { export {
DropdownMenu, DropdownMenu,
DropdownMenuPortal, DropdownMenuPortal,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
DropdownMenuRadioItem, DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuSubContent, DropdownMenuSubContent,
}; };
+104 -104
View File
@@ -1,104 +1,104 @@
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Empty({ className, ...props }: React.ComponentProps<"div">) { function Empty({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="empty" data-slot="empty"
className={cn( 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", "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, className,
)} )}
{...props} {...props}
/> />
); );
} }
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="empty-header" data-slot="empty-header"
className={cn( className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center", "flex max-w-sm flex-col items-center gap-2 text-center",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
const emptyMediaVariants = cva( const emptyMediaVariants = cva(
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0", "flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-transparent", 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 dark:text-[#55A6FE] dark:bg-[#022248]", icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6 dark:text-[#55A6FE] dark:bg-[#022248]",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
}, },
); );
function EmptyMedia({ function EmptyMedia({
className, className,
variant = "default", variant = "default",
...props ...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) { }: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return ( return (
<div <div
data-slot="empty-icon" data-slot="empty-icon"
data-variant={variant} data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))} className={cn(emptyMediaVariants({ variant, className }))}
{...props} {...props}
/> />
); );
} }
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="empty-title" data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight dark:text-[#AACEFF]", className)} className={cn("text-lg font-medium tracking-tight dark:text-[#AACEFF]", className)}
{...props} {...props}
/> />
); );
} }
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return ( return (
<div <div
data-slot="empty-description" data-slot="empty-description"
className={cn( className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 dark:text-[#55A6FE]", "text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 dark:text-[#55A6FE]",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="empty-content" data-slot="empty-content"
className={cn( className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance", "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
export { export {
Empty, Empty,
EmptyHeader, EmptyHeader,
EmptyTitle, EmptyTitle,
EmptyDescription, EmptyDescription,
EmptyContent, EmptyContent,
EmptyMedia, EmptyMedia,
}; };
+202 -202
View File
@@ -1,202 +1,202 @@
"use client"; "use client";
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> { interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> {
squareSize?: number; squareSize?: number;
gridGap?: number; gridGap?: number;
flickerChance?: number; flickerChance?: number;
color?: string; color?: string;
width?: number; width?: number;
height?: number; height?: number;
className?: string; className?: string;
maxOpacity?: number; maxOpacity?: number;
} }
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
squareSize = 4, squareSize = 4,
gridGap = 6, gridGap = 6,
flickerChance = 0.3, flickerChance = 0.3,
color = "rgb(0, 0, 0)", color = "rgb(0, 0, 0)",
width, width,
height, height,
className, className,
maxOpacity = 0.3, maxOpacity = 0.3,
...props ...props
}) => { }) => {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [isInView, setIsInView] = useState(false); const [isInView, setIsInView] = useState(false);
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }); const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
const memoizedColor = useMemo(() => { const memoizedColor = useMemo(() => {
const toRGBA = (color: string) => { const toRGBA = (color: string) => {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return `rgba(0, 0, 0,`; return `rgba(0, 0, 0,`;
} }
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = canvas.height = 1; canvas.width = canvas.height = 1;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (!ctx) return "rgba(255, 0, 0,"; if (!ctx) return "rgba(255, 0, 0,";
ctx.fillStyle = color; ctx.fillStyle = color;
ctx.fillRect(0, 0, 1, 1); ctx.fillRect(0, 0, 1, 1);
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data); const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
return `rgba(${r}, ${g}, ${b},`; return `rgba(${r}, ${g}, ${b},`;
}; };
return toRGBA(color); return toRGBA(color);
}, [color]); }, [color]);
const setupCanvas = useCallback( const setupCanvas = useCallback(
(canvas: HTMLCanvasElement, width: number, height: number) => { (canvas: HTMLCanvasElement, width: number, height: number) => {
const dpr = window.devicePixelRatio || 1; const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr; canvas.width = width * dpr;
canvas.height = height * dpr; canvas.height = height * dpr;
canvas.style.width = `${width}px`; canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`; canvas.style.height = `${height}px`;
const cols = Math.floor(width / (squareSize + gridGap)); const cols = Math.floor(width / (squareSize + gridGap));
const rows = Math.floor(height / (squareSize + gridGap)); const rows = Math.floor(height / (squareSize + gridGap));
const squares = new Float32Array(cols * rows); const squares = new Float32Array(cols * rows);
for (let i = 0; i < squares.length; i++) { for (let i = 0; i < squares.length; i++) {
squares[i] = Math.random() * maxOpacity; squares[i] = Math.random() * maxOpacity;
} }
return { cols, rows, squares, dpr }; return { cols, rows, squares, dpr };
}, },
[squareSize, gridGap, maxOpacity], [squareSize, gridGap, maxOpacity],
); );
const updateSquares = useCallback( const updateSquares = useCallback(
(squares: Float32Array, deltaTime: number) => { (squares: Float32Array, deltaTime: number) => {
for (let i = 0; i < squares.length; i++) { for (let i = 0; i < squares.length; i++) {
if (Math.random() < flickerChance * deltaTime) { if (Math.random() < flickerChance * deltaTime) {
squares[i] = Math.random() * maxOpacity; squares[i] = Math.random() * maxOpacity;
} }
} }
}, },
[flickerChance, maxOpacity], [flickerChance, maxOpacity],
); );
const drawGrid = useCallback( const drawGrid = useCallback(
( (
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
width: number, width: number,
height: number, height: number,
cols: number, cols: number,
rows: number, rows: number,
squares: Float32Array, squares: Float32Array,
dpr: number, dpr: number,
) => { ) => {
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "transparent"; ctx.fillStyle = "transparent";
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
for (let i = 0; i < cols; i++) { for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) { for (let j = 0; j < rows; j++) {
const opacity = squares[i * rows + j]; const opacity = squares[i * rows + j];
ctx.fillStyle = `${memoizedColor}${opacity})`; ctx.fillStyle = `${memoizedColor}${opacity})`;
ctx.fillRect( ctx.fillRect(
i * (squareSize + gridGap) * dpr, i * (squareSize + gridGap) * dpr,
j * (squareSize + gridGap) * dpr, j * (squareSize + gridGap) * dpr,
squareSize * dpr, squareSize * dpr,
squareSize * dpr, squareSize * dpr,
); );
} }
} }
}, },
[memoizedColor, squareSize, gridGap], [memoizedColor, squareSize, gridGap],
); );
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
const container = containerRef.current; const container = containerRef.current;
if (!canvas || !container) return; if (!canvas || !container) return;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (!ctx) return; if (!ctx) return;
let animationFrameId: number; let animationFrameId: number;
let gridParams: ReturnType<typeof setupCanvas>; let gridParams: ReturnType<typeof setupCanvas>;
const updateCanvasSize = () => { const updateCanvasSize = () => {
const newWidth = width || container.clientWidth; const newWidth = width || container.clientWidth;
const newHeight = height || container.clientHeight; const newHeight = height || container.clientHeight;
setCanvasSize({ width: newWidth, height: newHeight }); setCanvasSize({ width: newWidth, height: newHeight });
gridParams = setupCanvas(canvas, newWidth, newHeight); gridParams = setupCanvas(canvas, newWidth, newHeight);
}; };
updateCanvasSize(); updateCanvasSize();
let lastTime = 0; let lastTime = 0;
const animate = (time: number) => { const animate = (time: number) => {
if (!isInView) return; if (!isInView) return;
const deltaTime = (time - lastTime) / 1000; const deltaTime = (time - lastTime) / 1000;
lastTime = time; lastTime = time;
updateSquares(gridParams.squares, deltaTime); updateSquares(gridParams.squares, deltaTime);
drawGrid( drawGrid(
ctx, ctx,
canvas.width, canvas.width,
canvas.height, canvas.height,
gridParams.cols, gridParams.cols,
gridParams.rows, gridParams.rows,
gridParams.squares, gridParams.squares,
gridParams.dpr, gridParams.dpr,
); );
animationFrameId = requestAnimationFrame(animate); animationFrameId = requestAnimationFrame(animate);
}; };
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
updateCanvasSize(); updateCanvasSize();
}); });
resizeObserver.observe(container); resizeObserver.observe(container);
const intersectionObserver = new IntersectionObserver( const intersectionObserver = new IntersectionObserver(
([entry]) => { ([entry]) => {
if (entry) { if (entry) {
setIsInView(entry.isIntersecting); setIsInView(entry.isIntersecting);
} }
}, },
{ threshold: 0 }, { threshold: 0 },
); );
intersectionObserver.observe(canvas); intersectionObserver.observe(canvas);
if (isInView) { if (isInView) {
animationFrameId = requestAnimationFrame(animate); animationFrameId = requestAnimationFrame(animate);
} }
return () => { return () => {
cancelAnimationFrame(animationFrameId); cancelAnimationFrame(animationFrameId);
resizeObserver.disconnect(); resizeObserver.disconnect();
intersectionObserver.disconnect(); intersectionObserver.disconnect();
}; };
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]); }, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className={cn(`h-full w-full ${className}`)} className={cn(`h-full w-full ${className}`)}
{...props} {...props}
> >
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className="pointer-events-none" className="pointer-events-none"
style={{ style={{
width: canvasSize.width, width: canvasSize.width,
height: canvasSize.height, height: canvasSize.height,
}} }}
/> />
</div> </div>
); );
}; };
+5 -5
View File
@@ -1,5 +1,5 @@
.galaxy-container { .galaxy-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
} }
+361 -361
View File
@@ -1,361 +1,361 @@
import { Renderer, Program, Mesh, Color, Triangle } from "ogl"; import { Renderer, Program, Mesh, Color, Triangle } from "ogl";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import "./galaxy.css"; import "./galaxy.css";
const vertexShader = ` const vertexShader = `
attribute vec2 uv; attribute vec2 uv;
attribute vec2 position; attribute vec2 position;
varying vec2 vUv; varying vec2 vUv;
void main() { void main() {
vUv = uv; vUv = uv;
gl_Position = vec4(position, 0, 1); gl_Position = vec4(position, 0, 1);
} }
`; `;
const fragmentShader = ` const fragmentShader = `
precision highp float; precision highp float;
uniform float uTime; uniform float uTime;
uniform vec3 uResolution; uniform vec3 uResolution;
uniform vec2 uFocal; uniform vec2 uFocal;
uniform vec2 uRotation; uniform vec2 uRotation;
uniform float uStarSpeed; uniform float uStarSpeed;
uniform float uDensity; uniform float uDensity;
uniform float uHueShift; uniform float uHueShift;
uniform float uSpeed; uniform float uSpeed;
uniform vec2 uMouse; uniform vec2 uMouse;
uniform float uGlowIntensity; uniform float uGlowIntensity;
uniform float uSaturation; uniform float uSaturation;
uniform bool uMouseRepulsion; uniform bool uMouseRepulsion;
uniform float uTwinkleIntensity; uniform float uTwinkleIntensity;
uniform float uRotationSpeed; uniform float uRotationSpeed;
uniform float uRepulsionStrength; uniform float uRepulsionStrength;
uniform float uMouseActiveFactor; uniform float uMouseActiveFactor;
uniform float uAutoCenterRepulsion; uniform float uAutoCenterRepulsion;
uniform bool uTransparent; uniform bool uTransparent;
varying vec2 vUv; varying vec2 vUv;
#define NUM_LAYER 4.0 #define NUM_LAYER 4.0
#define STAR_COLOR_CUTOFF 0.2 #define STAR_COLOR_CUTOFF 0.2
#define MAT45 mat2(0.7071, -0.7071, 0.7071, 0.7071) #define MAT45 mat2(0.7071, -0.7071, 0.7071, 0.7071)
#define PERIOD 3.0 #define PERIOD 3.0
float Hash21(vec2 p) { float Hash21(vec2 p) {
p = fract(p * vec2(123.34, 456.21)); p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32); p += dot(p, p + 45.32);
return fract(p.x * p.y); return fract(p.x * p.y);
} }
float tri(float x) { float tri(float x) {
return abs(fract(x) * 2.0 - 1.0); return abs(fract(x) * 2.0 - 1.0);
} }
float tris(float x) { float tris(float x) {
float t = fract(x); float t = fract(x);
return 1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0)); return 1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0));
} }
float trisn(float x) { float trisn(float x) {
float t = fract(x); float t = fract(x);
return 2.0 * (1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0))) - 1.0; return 2.0 * (1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0))) - 1.0;
} }
vec3 hsv2rgb(vec3 c) { vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 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); 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); return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
} }
float Star(vec2 uv, float flare) { float Star(vec2 uv, float flare) {
float d = length(uv); float d = length(uv);
float m = (0.05 * uGlowIntensity) / d; float m = (0.05 * uGlowIntensity) / d;
float rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0)); float rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));
m += rays * flare * uGlowIntensity; m += rays * flare * uGlowIntensity;
uv *= MAT45; uv *= MAT45;
rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0)); rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));
m += rays * 0.3 * flare * uGlowIntensity; m += rays * 0.3 * flare * uGlowIntensity;
m *= smoothstep(1.0, 0.2, d); m *= smoothstep(1.0, 0.2, d);
return m; return m;
} }
vec3 StarLayer(vec2 uv) { vec3 StarLayer(vec2 uv) {
vec3 col = vec3(0.0); vec3 col = vec3(0.0);
vec2 gv = fract(uv) - 0.5; vec2 gv = fract(uv) - 0.5;
vec2 id = floor(uv); vec2 id = floor(uv);
for (int y = -1; y <= 1; y++) { for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) { for (int x = -1; x <= 1; x++) {
vec2 offset = vec2(float(x), float(y)); vec2 offset = vec2(float(x), float(y));
vec2 si = id + vec2(float(x), float(y)); vec2 si = id + vec2(float(x), float(y));
float seed = Hash21(si); float seed = Hash21(si);
float size = fract(seed * 345.32); float size = fract(seed * 345.32);
float glossLocal = tri(uStarSpeed / (PERIOD * seed + 1.0)); float glossLocal = tri(uStarSpeed / (PERIOD * seed + 1.0));
float flareSize = smoothstep(0.9, 1.0, size) * glossLocal; 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 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 blu = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 3.0)) + STAR_COLOR_CUTOFF;
float grn = min(red, blu) * seed; float grn = min(red, blu) * seed;
vec3 base = vec3(red, grn, blu); vec3 base = vec3(red, grn, blu);
float hue = atan(base.g - base.r, base.b - base.r) / (2.0 * 3.14159) + 0.5; float hue = atan(base.g - base.r, base.b - base.r) / (2.0 * 3.14159) + 0.5;
hue = fract(hue + uHueShift / 360.0); hue = fract(hue + uHueShift / 360.0);
float sat = length(base - vec3(dot(base, vec3(0.299, 0.587, 0.114)))) * uSaturation; 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); float val = max(max(base.r, base.g), base.b);
base = hsv2rgb(vec3(hue, sat, val)); 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; 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); float star = Star(gv - offset - pad, flareSize);
vec3 color = base; vec3 color = base;
float twinkle = trisn(uTime * uSpeed + seed * 6.2831) * 0.5 + 1.0; float twinkle = trisn(uTime * uSpeed + seed * 6.2831) * 0.5 + 1.0;
twinkle = mix(1.0, twinkle, uTwinkleIntensity); twinkle = mix(1.0, twinkle, uTwinkleIntensity);
star *= twinkle; star *= twinkle;
col += star * size * color; col += star * size * color;
} }
} }
return col; return col;
} }
void main() { void main() {
vec2 focalPx = uFocal * uResolution.xy; vec2 focalPx = uFocal * uResolution.xy;
vec2 uv = (vUv * uResolution.xy - focalPx) / uResolution.y; vec2 uv = (vUv * uResolution.xy - focalPx) / uResolution.y;
vec2 mouseNorm = uMouse - vec2(0.5); vec2 mouseNorm = uMouse - vec2(0.5);
if (uAutoCenterRepulsion > 0.0) { if (uAutoCenterRepulsion > 0.0) {
vec2 centerUV = vec2(0.0, 0.0); vec2 centerUV = vec2(0.0, 0.0);
float centerDist = length(uv - centerUV); float centerDist = length(uv - centerUV);
vec2 repulsion = normalize(uv - centerUV) * (uAutoCenterRepulsion / (centerDist + 0.1)); vec2 repulsion = normalize(uv - centerUV) * (uAutoCenterRepulsion / (centerDist + 0.1));
uv += repulsion * 0.05; uv += repulsion * 0.05;
} else if (uMouseRepulsion) { } else if (uMouseRepulsion) {
vec2 mousePosUV = (uMouse * uResolution.xy - focalPx) / uResolution.y; vec2 mousePosUV = (uMouse * uResolution.xy - focalPx) / uResolution.y;
float mouseDist = length(uv - mousePosUV); float mouseDist = length(uv - mousePosUV);
vec2 repulsion = normalize(uv - mousePosUV) * (uRepulsionStrength / (mouseDist + 0.1)); vec2 repulsion = normalize(uv - mousePosUV) * (uRepulsionStrength / (mouseDist + 0.1));
uv += repulsion * 0.05 * uMouseActiveFactor; uv += repulsion * 0.05 * uMouseActiveFactor;
} else { } else {
vec2 mouseOffset = mouseNorm * 0.1 * uMouseActiveFactor; vec2 mouseOffset = mouseNorm * 0.1 * uMouseActiveFactor;
uv += mouseOffset; uv += mouseOffset;
} }
float autoRotAngle = uTime * uRotationSpeed; float autoRotAngle = uTime * uRotationSpeed;
mat2 autoRot = mat2(cos(autoRotAngle), -sin(autoRotAngle), sin(autoRotAngle), cos(autoRotAngle)); mat2 autoRot = mat2(cos(autoRotAngle), -sin(autoRotAngle), sin(autoRotAngle), cos(autoRotAngle));
uv = autoRot * uv; uv = autoRot * uv;
uv = mat2(uRotation.x, -uRotation.y, uRotation.y, uRotation.x) * uv; uv = mat2(uRotation.x, -uRotation.y, uRotation.y, uRotation.x) * uv;
vec3 col = vec3(0.0); vec3 col = vec3(0.0);
for (float i = 0.0; i < 1.0; i += 1.0 / NUM_LAYER) { for (float i = 0.0; i < 1.0; i += 1.0 / NUM_LAYER) {
float depth = fract(i + uStarSpeed * uSpeed); float depth = fract(i + uStarSpeed * uSpeed);
float scale = mix(20.0 * uDensity, 0.5 * uDensity, depth); float scale = mix(20.0 * uDensity, 0.5 * uDensity, depth);
float fade = depth * smoothstep(1.0, 0.9, depth); float fade = depth * smoothstep(1.0, 0.9, depth);
col += StarLayer(uv * scale + i * 453.32) * fade; col += StarLayer(uv * scale + i * 453.32) * fade;
} }
if (uTransparent) { if (uTransparent) {
float alpha = length(col); float alpha = length(col);
alpha = smoothstep(0.0, 0.3, alpha); alpha = smoothstep(0.0, 0.3, alpha);
alpha = min(alpha, 1.0); alpha = min(alpha, 1.0);
gl_FragColor = vec4(col, alpha); gl_FragColor = vec4(col, alpha);
} else { } else {
gl_FragColor = vec4(col, 1.0); gl_FragColor = vec4(col, 1.0);
} }
} }
`; `;
export default function Galaxy({ export default function Galaxy({
focal = [0.5, 0.5], focal = [0.5, 0.5],
rotation = [1.0, 0.0], rotation = [1.0, 0.0],
starSpeed = 0.5, starSpeed = 0.5,
density = 1, density = 1,
hueShift = 140, hueShift = 140,
disableAnimation = false, disableAnimation = false,
speed = 1.0, speed = 1.0,
mouseInteraction = true, mouseInteraction = true,
glowIntensity = 0.3, glowIntensity = 0.3,
saturation = 0.0, saturation = 0.0,
mouseRepulsion = true, mouseRepulsion = true,
repulsionStrength = 2, repulsionStrength = 2,
twinkleIntensity = 0.3, twinkleIntensity = 0.3,
rotationSpeed = 0.1, rotationSpeed = 0.1,
autoCenterRepulsion = 0, autoCenterRepulsion = 0,
transparent = true, transparent = true,
...rest ...rest
}) { }) {
const ctnDom = useRef(null); const ctnDom = useRef(null);
const targetMousePos = useRef({ x: 0.5, y: 0.5 }); const targetMousePos = useRef({ x: 0.5, y: 0.5 });
const smoothMousePos = useRef({ x: 0.5, y: 0.5 }); const smoothMousePos = useRef({ x: 0.5, y: 0.5 });
const targetMouseActive = useRef(0.0); const targetMouseActive = useRef(0.0);
const smoothMouseActive = useRef(0.0); const smoothMouseActive = useRef(0.0);
useEffect(() => { useEffect(() => {
if (!ctnDom.current) return; if (!ctnDom.current) return;
const ctn = ctnDom.current; const ctn = ctnDom.current;
let renderer; let renderer;
try { try {
renderer = new Renderer({ renderer = new Renderer({
alpha: transparent, alpha: transparent,
premultipliedAlpha: false, premultipliedAlpha: false,
}); });
} catch (error) { } catch (error) {
console.warn( console.warn(
"Galaxy: WebGL is not available. The galaxy background will not be rendered.", "Galaxy: WebGL is not available. The galaxy background will not be rendered.",
error, error,
); );
return; return;
} }
const gl = renderer.gl; const gl = renderer.gl;
if (!gl) { if (!gl) {
console.warn( console.warn(
"Galaxy: WebGL context is null. The galaxy background will not be rendered.", "Galaxy: WebGL context is null. The galaxy background will not be rendered.",
); );
return; return;
} }
if (transparent) { if (transparent) {
gl.enable(gl.BLEND); gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0, 0, 0, 0); gl.clearColor(0, 0, 0, 0);
} else { } else {
gl.clearColor(0, 0, 0, 1); gl.clearColor(0, 0, 0, 1);
} }
/** @type {Program | undefined} */ /** @type {Program | undefined} */
let program; let program;
function resize() { function resize() {
const scale = 1; const scale = 1;
renderer.setSize(ctn.offsetWidth * scale, ctn.offsetHeight * scale); renderer.setSize(ctn.offsetWidth * scale, ctn.offsetHeight * scale);
if (program) { if (program) {
program.uniforms.uResolution.value = new Color( program.uniforms.uResolution.value = new Color(
gl.canvas.width, gl.canvas.width,
gl.canvas.height, gl.canvas.height,
gl.canvas.width / gl.canvas.height, gl.canvas.width / gl.canvas.height,
); );
} }
} }
window.addEventListener("resize", resize, false); window.addEventListener("resize", resize, false);
resize(); resize();
const geometry = new Triangle(gl); const geometry = new Triangle(gl);
program = new Program(gl, { program = new Program(gl, {
vertex: vertexShader, vertex: vertexShader,
fragment: fragmentShader, fragment: fragmentShader,
uniforms: { uniforms: {
uTime: { value: 0 }, uTime: { value: 0 },
uResolution: { uResolution: {
value: new Color( value: new Color(
gl.canvas.width, gl.canvas.width,
gl.canvas.height, gl.canvas.height,
gl.canvas.width / gl.canvas.height, gl.canvas.width / gl.canvas.height,
), ),
}, },
uFocal: { value: new Float32Array(focal) }, uFocal: { value: new Float32Array(focal) },
uRotation: { value: new Float32Array(rotation) }, uRotation: { value: new Float32Array(rotation) },
uStarSpeed: { value: starSpeed }, uStarSpeed: { value: starSpeed },
uDensity: { value: density }, uDensity: { value: density },
uHueShift: { value: hueShift }, uHueShift: { value: hueShift },
uSpeed: { value: speed }, uSpeed: { value: speed },
uMouse: { uMouse: {
value: new Float32Array([ value: new Float32Array([
smoothMousePos.current.x, smoothMousePos.current.x,
smoothMousePos.current.y, smoothMousePos.current.y,
]), ]),
}, },
uGlowIntensity: { value: glowIntensity }, uGlowIntensity: { value: glowIntensity },
uSaturation: { value: saturation }, uSaturation: { value: saturation },
uMouseRepulsion: { value: mouseRepulsion }, uMouseRepulsion: { value: mouseRepulsion },
uTwinkleIntensity: { value: twinkleIntensity }, uTwinkleIntensity: { value: twinkleIntensity },
uRotationSpeed: { value: rotationSpeed }, uRotationSpeed: { value: rotationSpeed },
uRepulsionStrength: { value: repulsionStrength }, uRepulsionStrength: { value: repulsionStrength },
uMouseActiveFactor: { value: 0.0 }, uMouseActiveFactor: { value: 0.0 },
uAutoCenterRepulsion: { value: autoCenterRepulsion }, uAutoCenterRepulsion: { value: autoCenterRepulsion },
uTransparent: { value: transparent }, uTransparent: { value: transparent },
}, },
}); });
const mesh = new Mesh(gl, { geometry, program }); const mesh = new Mesh(gl, { geometry, program });
let animateId; let animateId;
function update(t) { function update(t) {
animateId = requestAnimationFrame(update); animateId = requestAnimationFrame(update);
if (!disableAnimation) { if (!disableAnimation) {
program.uniforms.uTime.value = t * 0.001; program.uniforms.uTime.value = t * 0.001;
program.uniforms.uStarSpeed.value = (t * 0.001 * starSpeed) / 10.0; program.uniforms.uStarSpeed.value = (t * 0.001 * starSpeed) / 10.0;
} }
const lerpFactor = 0.05; const lerpFactor = 0.05;
smoothMousePos.current.x += smoothMousePos.current.x +=
(targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor; (targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor;
smoothMousePos.current.y += smoothMousePos.current.y +=
(targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor; (targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor;
smoothMouseActive.current += smoothMouseActive.current +=
(targetMouseActive.current - smoothMouseActive.current) * lerpFactor; (targetMouseActive.current - smoothMouseActive.current) * lerpFactor;
program.uniforms.uMouse.value[0] = smoothMousePos.current.x; program.uniforms.uMouse.value[0] = smoothMousePos.current.x;
program.uniforms.uMouse.value[1] = smoothMousePos.current.y; program.uniforms.uMouse.value[1] = smoothMousePos.current.y;
program.uniforms.uMouseActiveFactor.value = smoothMouseActive.current; program.uniforms.uMouseActiveFactor.value = smoothMouseActive.current;
renderer.render({ scene: mesh }); renderer.render({ scene: mesh });
} }
animateId = requestAnimationFrame(update); animateId = requestAnimationFrame(update);
ctn.appendChild(gl.canvas); ctn.appendChild(gl.canvas);
function handleMouseMove(e) { function handleMouseMove(e) {
const rect = ctn.getBoundingClientRect(); const rect = ctn.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width; const x = (e.clientX - rect.left) / rect.width;
const y = 1.0 - (e.clientY - rect.top) / rect.height; const y = 1.0 - (e.clientY - rect.top) / rect.height;
targetMousePos.current = { x, y }; targetMousePos.current = { x, y };
targetMouseActive.current = 1.0; targetMouseActive.current = 1.0;
} }
function handleMouseLeave() { function handleMouseLeave() {
targetMouseActive.current = 0.0; targetMouseActive.current = 0.0;
} }
if (mouseInteraction) { if (mouseInteraction) {
ctn.addEventListener("mousemove", handleMouseMove); ctn.addEventListener("mousemove", handleMouseMove);
ctn.addEventListener("mouseleave", handleMouseLeave); ctn.addEventListener("mouseleave", handleMouseLeave);
} }
return () => { return () => {
cancelAnimationFrame(animateId); cancelAnimationFrame(animateId);
window.removeEventListener("resize", resize); window.removeEventListener("resize", resize);
if (mouseInteraction) { if (mouseInteraction) {
ctn.removeEventListener("mousemove", handleMouseMove); ctn.removeEventListener("mousemove", handleMouseMove);
ctn.removeEventListener("mouseleave", handleMouseLeave); ctn.removeEventListener("mouseleave", handleMouseLeave);
} }
ctn.removeChild(gl.canvas); ctn.removeChild(gl.canvas);
gl.getExtension("WEBGL_lose_context")?.loseContext(); gl.getExtension("WEBGL_lose_context")?.loseContext();
}; };
}, [ }, [
focal, focal,
rotation, rotation,
starSpeed, starSpeed,
density, density,
hueShift, hueShift,
disableAnimation, disableAnimation,
speed, speed,
mouseInteraction, mouseInteraction,
glowIntensity, glowIntensity,
saturation, saturation,
mouseRepulsion, mouseRepulsion,
twinkleIntensity, twinkleIntensity,
rotationSpeed, rotationSpeed,
repulsionStrength, repulsionStrength,
autoCenterRepulsion, autoCenterRepulsion,
transparent, transparent,
]); ]);
return <div ref={ctnDom} className="galaxy-container" {...rest} />; return <div ref={ctnDom} className="galaxy-container" {...rest} />;
} }
+15 -15
View File
@@ -1,16 +1,16 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function HoverCard({ function HoverCard({
...props ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) { }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />; return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
} }
const HoverCardTrigger = React.forwardRef< const HoverCardTrigger = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Trigger>, React.ElementRef<typeof HoverCardPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Trigger>
@@ -48,5 +48,5 @@ const HoverCardContent = React.forwardRef<
}); });
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent }; export { HoverCard, HoverCardTrigger, HoverCardContent };
+160 -160
View File
@@ -1,173 +1,173 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) { function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="input-group" data-slot="input-group"
role="group" role="group"
className={cn( className={cn(
"group/input-group relative flex w-full items-center rounded-md border border-input/50 bg-white/80 shadow-xs transition-[color,box-shadow] outline-none dark:border-[#1B477E] dark:bg-[#022248]", "group/input-group relative flex w-full items-center rounded-md border border-input/50 bg-white/80 shadow-xs transition-[color,box-shadow] outline-none dark:border-[#1B477E] dark:bg-[#022248]",
"h-9 min-w-0 has-[>textarea]:h-auto", "h-9 min-w-0 has-[>textarea]:h-auto",
// Variants based on alignment. // Variants based on alignment.
"has-[>[data-align=inline-start]]:[&>input]:pl-2", "has-[>[data-align=inline-start]]:[&>input]:pl-2",
"has-[>[data-align=inline-end]]:[&>input]:pr-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-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", "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
// Focus state. // 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]", "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. // 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", "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, className,
)} )}
{...props} {...props}
/> />
); );
} }
const inputGroupAddonVariants = cva( 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", "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: { variants: {
align: { align: {
"inline-start": "inline-start":
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]", "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
"inline-end": "inline-end":
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]", "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
"block-start": "block-start":
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5", "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
"block-end": "block-end":
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5", "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
}, },
}, },
defaultVariants: { defaultVariants: {
align: "inline-start", align: "inline-start",
}, },
}, },
); );
function InputGroupAddon({ function InputGroupAddon({
className, className,
align = "inline-start", align = "inline-start",
...props ...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) { }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return ( return (
<div <div
role="group" role="group"
data-slot="input-group-addon" data-slot="input-group-addon"
data-align={align} data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)} className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => { onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) { if ((e.target as HTMLElement).closest("button")) {
return; return;
} }
e.currentTarget.parentElement?.querySelector("input")?.focus(); e.currentTarget.parentElement?.querySelector("input")?.focus();
}} }}
{...props} {...props}
/> />
); );
} }
const inputGroupButtonVariants = cva( const inputGroupButtonVariants = cva(
"text-sm shadow-none flex gap-2 items-center", "text-sm shadow-none flex gap-2 items-center",
{ {
variants: { variants: {
size: { size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2", 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", sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
"icon-xs": "icon-xs":
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0", "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0", "icon-sm": "size-8 p-0 has-[>svg]:p-0",
}, },
}, },
defaultVariants: { defaultVariants: {
size: "xs", size: "xs",
}, },
}, },
); );
function InputGroupButton({ function InputGroupButton({
className, className,
type = "button", type = "button",
variant = "ghost", variant = "ghost",
size = "xs", size = "xs",
...props ...props
}: Omit<React.ComponentProps<typeof Button>, "size"> & }: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) { VariantProps<typeof inputGroupButtonVariants>) {
return ( return (
<Button <Button
type={type} type={type}
data-size={size} data-size={size}
variant={variant} variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)} className={cn(inputGroupButtonVariants({ size }), className)}
{...props} {...props}
/> />
); );
} }
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) { function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return ( return (
<span <span
className={cn( className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function InputGroupInput({ function InputGroupInput({
className, className,
...props ...props
}: React.ComponentProps<"input">) { }: React.ComponentProps<"input">) {
return ( return (
<Input <Input
data-slot="input-group-control" data-slot="input-group-control"
className={cn( className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent", "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
const InputGroupTextarea = React.forwardRef< const InputGroupTextarea = React.forwardRef<
HTMLTextAreaElement, HTMLTextAreaElement,
React.ComponentProps<"textarea"> React.ComponentProps<"textarea">
>(function InputGroupTextarea({ className, ...props }, ref) { >(function InputGroupTextarea({ className, ...props }, ref) {
return ( return (
<Textarea <Textarea
data-slot="input-group-control" data-slot="input-group-control"
ref={ref} ref={ref}
className={cn( className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent", "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
className, className,
)} )}
{...props} {...props}
/> />
); );
}); });
InputGroupTextarea.displayName = "InputGroupTextarea"; InputGroupTextarea.displayName = "InputGroupTextarea";
export { export {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupButton, InputGroupButton,
InputGroupText, InputGroupText,
InputGroupInput, InputGroupInput,
InputGroupTextarea, InputGroupTextarea,
}; };
+33 -33
View File
@@ -1,33 +1,33 @@
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) { function Input({ className, type, ...props }: React.ComponentProps<"input">) {
const usesDarkNativePicker = const usesDarkNativePicker =
type === "number" || type === "number" ||
type === "date" || type === "date" ||
type === "datetime-local" || type === "datetime-local" ||
type === "time" || type === "time" ||
type === "month" || type === "month" ||
type === "week"; type === "week";
return ( return (
<input <input
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm", "h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm",
"border-input placeholder:text-muted-foreground", "border-input placeholder:text-muted-foreground",
"dark:border-[#1B477E] dark:bg-[#0D2D54] dark:text-[#AACEFF] dark:placeholder:text-[#566B7F] dark:rounded-[4px] dark:text-[16px] md:dark:text-[16px]", "dark:border-[#1B477E] dark:bg-[#0D2D54] dark:text-[#AACEFF] dark:placeholder:text-[#566B7F] dark:rounded-[4px] dark:text-[16px] md:dark:text-[16px]",
usesDarkNativePicker && "dark:[color-scheme:dark]", usesDarkNativePicker && "dark:[color-scheme:dark]",
"file:text-foreground selection:bg-primary selection:text-primary-foreground 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", "file:text-foreground selection:bg-primary selection:text-primary-foreground 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",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
export { Input }; export { Input };
+193 -193
View File
@@ -1,193 +1,193 @@
import * as React from "react"; import * as React from "react";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) { function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
role="list" role="list"
data-slot="item-group" data-slot="item-group"
className={cn("group/item-group flex flex-col", className)} className={cn("group/item-group flex flex-col", className)}
{...props} {...props}
/> />
); );
} }
function ItemSeparator({ function ItemSeparator({
className, className,
...props ...props
}: React.ComponentProps<typeof Separator>) { }: React.ComponentProps<typeof Separator>) {
return ( return (
<Separator <Separator
data-slot="item-separator" data-slot="item-separator"
orientation="horizontal" orientation="horizontal"
className={cn("my-0", className)} className={cn("my-0", className)}
{...props} {...props}
/> />
); );
} }
const itemVariants = cva( 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]", "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: { variants: {
variant: { variant: {
default: "bg-transparent", default: "bg-transparent",
outline: "border-border", outline: "border-border",
muted: "bg-muted/50", muted: "bg-muted/50",
}, },
size: { size: {
default: "p-4 gap-4 ", default: "p-4 gap-4 ",
sm: "py-3 px-4 gap-2.5", sm: "py-3 px-4 gap-2.5",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default", size: "default",
}, },
}, },
); );
function Item({ function Item({
className, className,
variant = "default", variant = "default",
size = "default", size = "default",
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"div"> & }: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) { VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"; const Comp = asChild ? Slot : "div";
return ( return (
<Comp <Comp
data-slot="item" data-slot="item"
data-variant={variant} data-variant={variant}
data-size={size} data-size={size}
className={cn(itemVariants({ variant, size, className }))} className={cn(itemVariants({ variant, size, className }))}
{...props} {...props}
/> />
); );
} }
const itemMediaVariants = cva( 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", "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: { variants: {
variant: { variant: {
default: "bg-transparent", default: "bg-transparent",
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4", icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image: image:
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover", "size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
}, },
); );
function ItemMedia({ function ItemMedia({
className, className,
variant = "default", variant = "default",
...props ...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) { }: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
return ( return (
<div <div
data-slot="item-media" data-slot="item-media"
data-variant={variant} data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))} className={cn(itemMediaVariants({ variant, className }))}
{...props} {...props}
/> />
); );
} }
function ItemContent({ className, ...props }: React.ComponentProps<"div">) { function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="item-content" data-slot="item-content"
className={cn( className={cn(
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none", "flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) { function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="item-title" data-slot="item-title"
className={cn( className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium", "flex w-fit items-center gap-2 text-sm leading-snug font-medium",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) { function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return ( return (
<p <p
data-slot="item-description" data-slot="item-description"
className={cn( className={cn(
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance", "text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function ItemActions({ className, ...props }: React.ComponentProps<"div">) { function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="item-actions" data-slot="item-actions"
className={cn("flex items-center gap-2", className)} className={cn("flex items-center gap-2", className)}
{...props} {...props}
/> />
); );
} }
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) { function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="item-header" data-slot="item-header"
className={cn( className={cn(
"flex basis-full items-center justify-between gap-2", "flex basis-full items-center justify-between gap-2",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) { function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="item-footer" data-slot="item-footer"
className={cn( className={cn(
"flex basis-full items-center justify-between gap-2", "flex basis-full items-center justify-between gap-2",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
export { export {
Item, Item,
ItemMedia, ItemMedia,
ItemContent, ItemContent,
ItemActions, ItemActions,
ItemGroup, ItemGroup,
ItemSeparator, ItemSeparator,
ItemTitle, ItemTitle,
ItemDescription, ItemDescription,
ItemHeader, ItemHeader,
ItemFooter, ItemFooter,
}; };
+217 -217
View File
@@ -1,217 +1,217 @@
:root { :root {
--hue: 27; --hue: 27;
--sat: 69%; --sat: 69%;
--white: hsl(0, 0%, 100%); --white: hsl(0, 0%, 100%);
--purple-primary: rgba(132, 0, 255, 1); --purple-primary: rgba(132, 0, 255, 1);
--purple-glow: rgba(132, 0, 255, 0.2); --purple-glow: rgba(132, 0, 255, 0.2);
--purple-border: rgba(132, 0, 255, 0.8); --purple-border: rgba(132, 0, 255, 0.8);
--border-color: #392e4e; --border-color: #392e4e;
--background-dark: #060010; --background-dark: #060010;
color-scheme: light dark; color-scheme: light dark;
} }
.card-grid { .card-grid {
display: grid; display: grid;
gap: 0.5em; gap: 0.5em;
padding: 0.75em; padding: 0.75em;
max-width: 54em; max-width: 54em;
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.5rem); font-size: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);
} }
.magic-bento-card { .magic-bento-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
position: relative; position: relative;
aspect-ratio: 4/3; aspect-ratio: 4/3;
min-height: 200px; min-height: 200px;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
padding: 1.25em; padding: 1.25em;
border-radius: 20px; border-radius: 20px;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
background: var(--background-dark); background: var(--background-dark);
font-weight: 300; font-weight: 300;
overflow: hidden; overflow: hidden;
transition: all 0.3s ease; transition: all 0.3s ease;
--glow-x: 50%; --glow-x: 50%;
--glow-y: 50%; --glow-y: 50%;
--glow-intensity: 0; --glow-intensity: 0;
--glow-radius: 200px; --glow-radius: 200px;
} }
.magic-bento-card:hover { .magic-bento-card:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
} }
.magic-bento-card__header, .magic-bento-card__header,
.magic-bento-card__content { .magic-bento-card__content {
display: flex; display: flex;
position: relative; position: relative;
color: var(--white); color: var(--white);
} }
.magic-bento-card__header { .magic-bento-card__header {
gap: 0.75em; gap: 0.75em;
justify-content: space-between; justify-content: space-between;
} }
.magic-bento-card__content { .magic-bento-card__content {
flex-direction: column; flex-direction: column;
} }
.magic-bento-card__label { .magic-bento-card__label {
font-size: 16px; font-size: 16px;
} }
.magic-bento-card__title, .magic-bento-card__title,
.magic-bento-card__description { .magic-bento-card__description {
--clamp-title: 1; --clamp-title: 1;
--clamp-desc: 2; --clamp-desc: 2;
} }
.magic-bento-card__title { .magic-bento-card__title {
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
margin: 0 0 0.25em; margin: 0 0 0.25em;
} }
.magic-bento-card__description { .magic-bento-card__description {
font-size: 12px; font-size: 12px;
line-height: 1.2; line-height: 1.2;
opacity: 0.9; opacity: 0.9;
} }
.magic-bento-card--text-autohide .magic-bento-card__title, .magic-bento-card--text-autohide .magic-bento-card__title,
.magic-bento-card--text-autohide .magic-bento-card__description { .magic-bento-card--text-autohide .magic-bento-card__description {
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.magic-bento-card--text-autohide .magic-bento-card__title { .magic-bento-card--text-autohide .magic-bento-card__title {
-webkit-line-clamp: var(--clamp-title); -webkit-line-clamp: var(--clamp-title);
line-clamp: var(--clamp-title); line-clamp: var(--clamp-title);
} }
.magic-bento-card--text-autohide .magic-bento-card__description { .magic-bento-card--text-autohide .magic-bento-card__description {
-webkit-line-clamp: var(--clamp-desc); -webkit-line-clamp: var(--clamp-desc);
line-clamp: var(--clamp-desc); line-clamp: var(--clamp-desc);
} }
@media (max-width: 599px) { @media (max-width: 599px) {
.card-grid { .card-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
width: 90%; width: 90%;
margin: 0 auto; margin: 0 auto;
padding: 0.5em; padding: 0.5em;
} }
.magic-bento-card { .magic-bento-card {
width: 100%; width: 100%;
min-height: 180px; min-height: 180px;
} }
} }
@media (min-width: 600px) { @media (min-width: 600px) {
.card-grid { .card-grid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.card-grid { .card-grid {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
} }
.magic-bento-card:nth-child(3) { .magic-bento-card:nth-child(3) {
grid-column: span 2; grid-column: span 2;
grid-row: span 2; grid-row: span 2;
} }
.magic-bento-card:nth-child(4) { .magic-bento-card:nth-child(4) {
grid-column: 1 / span 2; grid-column: 1 / span 2;
grid-row: 2 / span 2; grid-row: 2 / span 2;
} }
.magic-bento-card:nth-child(6) { .magic-bento-card:nth-child(6) {
grid-column: 4; grid-column: 4;
grid-row: 3; grid-row: 3;
} }
} }
/* Border glow effect */ /* Border glow effect */
.magic-bento-card--border-glow::after { .magic-bento-card--border-glow::after {
content: ""; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
padding: 6px; padding: 6px;
background: radial-gradient( background: radial-gradient(
var(--glow-radius) circle at var(--glow-x) var(--glow-y), 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.8)) 0%,
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%, rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,
transparent 60% transparent 60%
); );
border-radius: inherit; border-radius: inherit;
-webkit-mask: -webkit-mask:
linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0); linear-gradient(#fff 0 0);
-webkit-mask-composite: xor; -webkit-mask-composite: xor;
mask: mask:
linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0); linear-gradient(#fff 0 0);
mask-composite: exclude; mask-composite: exclude;
pointer-events: none; pointer-events: none;
opacity: 1; opacity: 1;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
z-index: 1; z-index: 1;
} }
.magic-bento-card--border-glow:hover::after { .magic-bento-card--border-glow:hover::after {
opacity: 1; opacity: 1;
} }
.magic-bento-card--border-glow:hover { .magic-bento-card--border-glow:hover {
box-shadow: box-shadow:
0 4px 20px rgba(46, 24, 78, 0.4), 0 4px 20px rgba(46, 24, 78, 0.4),
0 0 30px var(--purple-glow); 0 0 30px var(--purple-glow);
} }
.particle-container { .particle-container {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.particle::before { .particle::before {
content: ""; content: "";
position: absolute; position: absolute;
top: -2px; top: -2px;
left: -2px; left: -2px;
right: -2px; right: -2px;
bottom: -2px; bottom: -2px;
background: rgba(132, 0, 255, 0.2); background: rgba(132, 0, 255, 0.2);
border-radius: 50%; border-radius: 50%;
z-index: -1; z-index: -1;
} }
.particle-container:hover { .particle-container:hover {
box-shadow: box-shadow:
0 4px 20px rgba(46, 24, 78, 0.2), 0 4px 20px rgba(46, 24, 78, 0.2),
0 0 30px var(--purple-glow); 0 0 30px var(--purple-glow);
} }
/* Global spotlight styles */ /* Global spotlight styles */
.global-spotlight { .global-spotlight {
mix-blend-mode: screen; mix-blend-mode: screen;
will-change: transform, opacity; will-change: transform, opacity;
z-index: 200 !important; z-index: 200 !important;
pointer-events: none; pointer-events: none;
} }
.bento-section { .bento-section {
position: relative; position: relative;
user-select: none; user-select: none;
} }
File diff suppressed because it is too large. Load diff
@@ -1,67 +1,67 @@
"use client"; "use client";
import { type ComponentPropsWithoutRef, useEffect, useRef } from "react"; import { type ComponentPropsWithoutRef, useEffect, useRef } from "react";
import { useInView, useMotionValue, useSpring } from "motion/react"; import { useInView, useMotionValue, useSpring } from "motion/react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> { interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> {
value: number; value: number;
startValue?: number; startValue?: number;
direction?: "up" | "down"; direction?: "up" | "down";
delay?: number; delay?: number;
decimalPlaces?: number; decimalPlaces?: number;
} }
export function NumberTicker({ export function NumberTicker({
value, value,
startValue = 0, startValue = 0,
direction = "up", direction = "up",
delay = 0, delay = 0,
className, className,
decimalPlaces = 0, decimalPlaces = 0,
...props ...props
}: NumberTickerProps) { }: NumberTickerProps) {
const ref = useRef<HTMLSpanElement>(null); const ref = useRef<HTMLSpanElement>(null);
const motionValue = useMotionValue(direction === "down" ? value : startValue); const motionValue = useMotionValue(direction === "down" ? value : startValue);
const springValue = useSpring(motionValue, { const springValue = useSpring(motionValue, {
damping: 60, damping: 60,
stiffness: 100, stiffness: 100,
}); });
const isInView = useInView(ref, { once: true, margin: "0px" }); const isInView = useInView(ref, { once: true, margin: "0px" });
useEffect(() => { useEffect(() => {
if (isInView) { if (isInView) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
motionValue.set(direction === "down" ? startValue : value); motionValue.set(direction === "down" ? startValue : value);
}, delay * 1000); }, delay * 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [motionValue, isInView, delay, value, direction, startValue]); }, [motionValue, isInView, delay, value, direction, startValue]);
useEffect( useEffect(
() => () =>
springValue.on("change", (latest) => { springValue.on("change", (latest) => {
if (ref.current) { if (ref.current) {
ref.current.textContent = Intl.NumberFormat("en-US", { ref.current.textContent = Intl.NumberFormat("en-US", {
minimumFractionDigits: decimalPlaces, minimumFractionDigits: decimalPlaces,
maximumFractionDigits: decimalPlaces, maximumFractionDigits: decimalPlaces,
}).format(Number(latest.toFixed(decimalPlaces))); }).format(Number(latest.toFixed(decimalPlaces)));
} }
}), }),
[springValue, decimalPlaces], [springValue, decimalPlaces],
); );
return ( return (
<span <span
ref={ref} ref={ref}
className={cn( className={cn(
"inline-block tracking-wider text-black tabular-nums dark:text-white", "inline-block tracking-wider text-black tabular-nums dark:text-white",
className, className,
)} )}
{...props} {...props}
> >
{startValue} {startValue}
</span> </span>
); );
} }
+31 -31
View File
@@ -1,31 +1,31 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress"; import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Progress({ function Progress({
className, className,
value, value,
...props ...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) { }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return ( return (
<ProgressPrimitive.Root <ProgressPrimitive.Root
data-slot="progress" data-slot="progress"
className={cn( className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className, className,
)} )}
{...props} {...props}
> >
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
data-slot="progress-indicator" data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all" className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
); );
} }
export { Progress }; export { Progress };
+82 -82
View File
@@ -1,82 +1,82 @@
"use client"; "use client";
import { GripVerticalIcon } from "lucide-react"; import { GripVerticalIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import * as ResizablePrimitive from "react-resizable-panels"; import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function ResizablePanelGroup({ function ResizablePanelGroup({
className, className,
...props ...props
}: React.ComponentProps<typeof ResizablePrimitive.Group>) { }: React.ComponentProps<typeof ResizablePrimitive.Group>) {
return ( return (
<ResizablePrimitive.Group <ResizablePrimitive.Group
data-slot="resizable-panel-group" data-slot="resizable-panel-group"
className={cn( className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col", "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
// react-resizable-panels v4 changed size semantics: a bare *number* is now // react-resizable-panels v4 changed size semantics: a bare *number* is now
// interpreted as PIXELS, while a unit-less *string* is a percentage. Every call // interpreted as PIXELS, while a unit-less *string* is a percentage. Every call
// site in this app passes sizes as percentages (the v2/v3 convention), so a // site in this app passes sizes as percentages (the v2/v3 convention), so a
// value like `maxSize={60}` would otherwise pin a panel to 60px and freeze the // value like `maxSize={60}` would otherwise pin a panel to 60px and freeze the
// layout/drag. Normalize numbers to "<n>%" to preserve the intended percentage. // layout/drag. Normalize numbers to "<n>%" to preserve the intended percentage.
const asPercent = (v: number | string | undefined) => const asPercent = (v: number | string | undefined) =>
typeof v === "number" ? `${v}%` : v; typeof v === "number" ? `${v}%` : v;
function ResizablePanel({ function ResizablePanel({
defaultSize, defaultSize,
minSize, minSize,
maxSize, maxSize,
collapsedSize, collapsedSize,
...props ...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) { }: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return ( return (
<ResizablePrimitive.Panel <ResizablePrimitive.Panel
data-slot="resizable-panel" data-slot="resizable-panel"
defaultSize={asPercent(defaultSize)} defaultSize={asPercent(defaultSize)}
minSize={asPercent(minSize)} minSize={asPercent(minSize)}
maxSize={asPercent(maxSize)} maxSize={asPercent(maxSize)}
collapsedSize={asPercent(collapsedSize)} collapsedSize={asPercent(collapsedSize)}
{...props} {...props}
/> />
); );
} }
function ResizableHandle({ function ResizableHandle({
withHandle, withHandle,
className, className,
...props ...props
}: React.ComponentProps<typeof ResizablePrimitive.Separator> & { }: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
withHandle?: boolean; withHandle?: boolean;
}) { }) {
return ( return (
<ResizablePrimitive.Separator <ResizablePrimitive.Separator
data-slot="resizable-handle" data-slot="resizable-handle"
className={cn( className={cn(
// Thin visible divider, but a generous invisible hit area on both sides // Thin visible divider, but a generous invisible hit area on both sides
// (after:w-4 → 16px) so the handle is easy to grab and drag. Hovering / // (after:w-4 → 16px) so the handle is easy to grab and drag. Hovering /
// dragging tints the divider so it's obvious it's interactive. // dragging tints the divider so it's obvious it's interactive.
"bg-border hover:bg-primary/60 active:bg-primary relative flex w-px items-center justify-center transition-colors", "bg-border hover:bg-primary/60 active:bg-primary relative flex w-px items-center justify-center transition-colors",
"after:absolute after:inset-y-0 after:left-1/2 after:w-4 after:-translate-x-1/2 after:cursor-col-resize", "after:absolute after:inset-y-0 after:left-1/2 after:w-4 after:-translate-x-1/2 after:cursor-col-resize",
"focus-visible:ring-ring focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden", "focus-visible:ring-ring focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden",
className, className,
)} )}
{...props} {...props}
> >
{withHandle && ( {withHandle && (
<div className="bg-border z-10 flex h-8 w-4 items-center justify-center rounded-sm border shadow-sm"> <div className="bg-border z-10 flex h-8 w-4 items-center justify-center rounded-sm border shadow-sm">
<GripVerticalIcon className="size-3.5" /> <GripVerticalIcon className="size-3.5" />
</div> </div>
)} )}
</ResizablePrimitive.Separator> </ResizablePrimitive.Separator>
); );
} }
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
+58 -58
View File
@@ -1,58 +1,58 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function ScrollArea({ function ScrollArea({
className, className,
children, children,
...props ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) { }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return ( return (
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
data-slot="scroll-area" data-slot="scroll-area"
className={cn("relative", className)} className={cn("relative", className)}
{...props} {...props}
> >
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport
data-slot="scroll-area-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" 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} {children}
</ScrollAreaPrimitive.Viewport> </ScrollAreaPrimitive.Viewport>
<ScrollBar /> <ScrollBar />
<ScrollAreaPrimitive.Corner /> <ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root> </ScrollAreaPrimitive.Root>
); );
} }
function ScrollBar({ function ScrollBar({
className, className,
orientation = "vertical", orientation = "vertical",
...props ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) { }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return ( return (
<ScrollAreaPrimitive.ScrollAreaScrollbar <ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar" data-slot="scroll-area-scrollbar"
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"flex touch-none p-px transition-colors select-none", "flex touch-none p-px transition-colors select-none",
orientation === "vertical" && orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent", "h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" && orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent", "h-2.5 flex-col border-t border-t-transparent",
className, className,
)} )}
{...props} {...props}
> >
<ScrollAreaPrimitive.ScrollAreaThumb <ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb" data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full" className="bg-border relative flex-1 rounded-full"
/> />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </ScrollAreaPrimitive.ScrollAreaScrollbar>
); );
} }
export { ScrollArea, ScrollBar }; export { ScrollArea, ScrollBar };
+198 -198
View File
@@ -1,198 +1,198 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select"; import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Select({ function Select({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) { }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />; return <SelectPrimitive.Root data-slot="select" {...props} />;
} }
function SelectGroup({ function SelectGroup({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) { }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />; return <SelectPrimitive.Group data-slot="select-group" {...props} />;
} }
function SelectValue({ function SelectValue({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) { }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />; return <SelectPrimitive.Value data-slot="select-value" {...props} />;
} }
const SelectTrigger = React.forwardRef< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & { React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"; size?: "sm" | "default";
} }
>(({ className, size = "default", children, ...props }, ref) => { >(({ className, size = "default", children, ...props }, ref) => {
return ( return (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( 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 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", "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 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",
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEE2] dark:data-[placeholder]:text-[#566B7F] dark:hover:bg-[#022248] dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30", "dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEE2] dark:data-[placeholder]:text-[#566B7F] dark:hover:bg-[#022248] dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<SelectPrimitive.Icon asChild> <SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" /> <ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
); );
}); });
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "item-aligned", align = "center", ...props }, ref) => { >(({ className, children, position = "item-aligned", align = "center", ...props }, ref) => {
return ( return (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
ref={ref} ref={ref}
data-slot="select-content" data-slot="select-content"
className={cn( 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 dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF]", "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 dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF]",
position === "popper" && 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", "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, className,
)} )}
position={position} position={position}
align={align} align={align}
{...props} {...props}
> >
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1", "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)} )}
> >
{children} {children}
</SelectPrimitive.Viewport> </SelectPrimitive.Viewport>
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
); );
}); });
SelectContent.displayName = SelectPrimitive.Content.displayName; SelectContent.displayName = SelectPrimitive.Content.displayName;
function SelectLabel({ function SelectLabel({
className, className,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) { }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return ( return (
<SelectPrimitive.Label <SelectPrimitive.Label
data-slot="select-label" data-slot="select-label"
className={cn( className={cn(
"text-muted-foreground px-2 py-1.5 text-xs dark:text-[#566B7F]", "text-muted-foreground px-2 py-1.5 text-xs dark:text-[#566B7F]",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
const SelectItem = React.forwardRef< const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>, React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => { >(({ className, children, ...props }, ref) => {
return ( return (
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
data-slot="select-item" data-slot="select-item"
className={cn( 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", "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",
"dark:text-[#AACEFF] dark:focus:bg-[#0062D9] dark:focus:text-white dark:data-[highlighted]:bg-[#0062D9] dark:data-[highlighted]:text-white dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus:[&_svg:not([class*='text-'])]:text-white dark:data-[highlighted]:[&_svg:not([class*='text-'])]:text-white", "dark:text-[#AACEFF] dark:focus:bg-[#0062D9] dark:focus:text-white dark:data-[highlighted]:bg-[#0062D9] dark:data-[highlighted]:text-white dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus:[&_svg:not([class*='text-'])]:text-white dark:data-[highlighted]:[&_svg:not([class*='text-'])]:text-white",
className, className,
)} )}
{...props} {...props}
> >
<span <span
data-slot="select-item-indicator" data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center" className="absolute right-2 flex size-3.5 items-center justify-center"
> >
<SelectPrimitive.ItemIndicator> <SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" /> <CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator> </SelectPrimitive.ItemIndicator>
</span> </span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
); );
}); });
SelectItem.displayName = SelectPrimitive.Item.displayName; SelectItem.displayName = SelectPrimitive.Item.displayName;
function SelectSeparator({ function SelectSeparator({
className, className,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) { }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return ( return (
<SelectPrimitive.Separator <SelectPrimitive.Separator
data-slot="select-separator" data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)} className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
); );
} }
function SelectScrollUpButton({ function SelectScrollUpButton({
className, className,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return ( return (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button" data-slot="select-scroll-up-button"
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]", "flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]",
className, className,
)} )}
{...props} {...props}
> >
<ChevronUpIcon className="size-4" /> <ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
); );
} }
function SelectScrollDownButton({ function SelectScrollDownButton({
className, className,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) { }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return ( return (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button" data-slot="select-scroll-down-button"
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]", "flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]",
className, className,
)} )}
{...props} {...props}
> >
<ChevronDownIcon className="size-4" /> <ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
); );
} }
export { export {
Select, Select,
SelectContent, SelectContent,
SelectGroup, SelectGroup,
SelectItem, SelectItem,
SelectLabel, SelectLabel,
SelectScrollDownButton, SelectScrollDownButton,
SelectScrollUpButton, SelectScrollUpButton,
SelectSeparator, SelectSeparator,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
}; };
+28 -28
View File
@@ -1,28 +1,28 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator"; import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Separator({ function Separator({
className, className,
orientation = "horizontal", orientation = "horizontal",
decorative = true, decorative = true,
...props ...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return ( return (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
data-slot="separator" data-slot="separator"
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( 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", "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, className,
)} )}
{...props} {...props}
/> />
); );
} }
export { Separator }; export { Separator };
+78 -78
View File
@@ -1,15 +1,15 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog"; import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react"; import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />; return <SheetPrimitive.Root data-slot="sheet" {...props} />;
} }
const SheetTrigger = React.forwardRef< const SheetTrigger = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Trigger>, React.ElementRef<typeof SheetPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof SheetPrimitive.Trigger>
@@ -18,7 +18,7 @@ const SheetTrigger = React.forwardRef<
}); });
SheetTrigger.displayName = SheetPrimitive.Trigger.displayName; SheetTrigger.displayName = SheetPrimitive.Trigger.displayName;
const SheetClose = React.forwardRef< const SheetClose = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Close>, React.ElementRef<typeof SheetPrimitive.Close>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Close> React.ComponentPropsWithoutRef<typeof SheetPrimitive.Close>
@@ -27,13 +27,13 @@ const SheetClose = React.forwardRef<
}); });
SheetClose.displayName = SheetPrimitive.Close.displayName; SheetClose.displayName = SheetPrimitive.Close.displayName;
function SheetPortal({ function SheetPortal({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) { }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />; return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
} }
const SheetOverlay = React.forwardRef< const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>, React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
@@ -52,7 +52,7 @@ const SheetOverlay = React.forwardRef<
}); });
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const SheetContent = React.forwardRef< const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>, React.ElementRef<typeof SheetPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & { React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & {
@@ -90,60 +90,60 @@ const SheetContent = React.forwardRef<
}); });
SheetContent.displayName = SheetPrimitive.Content.displayName; SheetContent.displayName = SheetPrimitive.Content.displayName;
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sheet-header" data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)} className={cn("flex flex-col gap-1.5 p-4", className)}
{...props} {...props}
/> />
); );
} }
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sheet-footer" data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...props}
/> />
); );
} }
function SheetTitle({ function SheetTitle({
className, className,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) { }: React.ComponentProps<typeof SheetPrimitive.Title>) {
return ( return (
<SheetPrimitive.Title <SheetPrimitive.Title
data-slot="sheet-title" data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)} className={cn("text-foreground font-semibold", className)}
{...props} {...props}
/> />
); );
} }
function SheetDescription({ function SheetDescription({
className, className,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) { }: React.ComponentProps<typeof SheetPrimitive.Description>) {
return ( return (
<SheetPrimitive.Description <SheetPrimitive.Description
data-slot="sheet-description" data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
); );
} }
export { export {
Sheet, Sheet,
SheetTrigger, SheetTrigger,
SheetClose, SheetClose,
SheetContent, SheetContent,
SheetHeader, SheetHeader,
SheetFooter, SheetFooter,
SheetTitle, SheetTitle,
SheetDescription, SheetDescription,
}; };
+63 -63
View File
@@ -1,63 +1,63 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface ShineBorderProps extends React.HTMLAttributes<HTMLDivElement> { interface ShineBorderProps extends React.HTMLAttributes<HTMLDivElement> {
/** /**
* Width of the border in pixels * Width of the border in pixels
* @default 1 * @default 1
*/ */
borderWidth?: number; borderWidth?: number;
/** /**
* Duration of the animation in seconds * Duration of the animation in seconds
* @default 14 * @default 14
*/ */
duration?: number; duration?: number;
/** /**
* Color of the border, can be a single color or an array of colors * Color of the border, can be a single color or an array of colors
* @default "#000000" * @default "#000000"
*/ */
shineColor?: string | string[]; shineColor?: string | string[];
} }
/** /**
* Shine Border * Shine Border
* *
* An animated background border effect component with configurable properties. * An animated background border effect component with configurable properties.
*/ */
export function ShineBorder({ export function ShineBorder({
borderWidth = 1, borderWidth = 1,
duration = 14, duration = 14,
shineColor = "#000000", shineColor = "#000000",
className, className,
style, style,
...props ...props
}: ShineBorderProps) { }: ShineBorderProps) {
return ( return (
<div <div
style={ style={
{ {
"--border-width": `${borderWidth}px`, "--border-width": `${borderWidth}px`,
"--duration": `${duration}s`, "--duration": `${duration}s`,
backgroundImage: `radial-gradient(transparent,transparent, ${ backgroundImage: `radial-gradient(transparent,transparent, ${
Array.isArray(shineColor) ? shineColor.join(",") : shineColor Array.isArray(shineColor) ? shineColor.join(",") : shineColor
},transparent,transparent)`, },transparent,transparent)`,
backgroundSize: "300% 300%", backgroundSize: "300% 300%",
mask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`, 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)`, WebkitMask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
WebkitMaskComposite: "xor", WebkitMaskComposite: "xor",
maskComposite: "exclude", maskComposite: "exclude",
padding: "var(--border-width)", padding: "var(--border-width)",
...style, ...style,
} as React.CSSProperties } as React.CSSProperties
} }
className={cn( className={cn(
"motion-safe:animate-shine pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position]", "motion-safe:animate-shine pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position]",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
File diff suppressed because it is too large. Load diff
+13 -13
View File
@@ -1,13 +1,13 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) { function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="skeleton" data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)} className={cn("bg-accent animate-pulse rounded-md", className)}
{...props} {...props}
/> />
); );
} }
export { Skeleton }; export { Skeleton };
+40 -40
View File
@@ -1,40 +1,40 @@
"use client"; "use client";
import { import {
CircleCheckIcon, CircleCheckIcon,
InfoIcon, InfoIcon,
Loader2Icon, Loader2Icon,
OctagonXIcon, OctagonXIcon,
TriangleAlertIcon, TriangleAlertIcon,
} from "lucide-react"; } from "lucide-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner"; import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme(); const { theme = "system" } = useTheme();
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme as ToasterProps["theme"]}
className="toaster group" className="toaster group"
icons={{ icons={{
success: <CircleCheckIcon className="size-4" />, success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />, info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />, warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />, error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />, loading: <Loader2Icon className="size-4 animate-spin" />,
}} }}
style={ style={
{ {
"--normal-bg": "var(--popover)", "--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)", "--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)", "--normal-border": "var(--border)",
"--border-radius": "var(--radius)", "--border-radius": "var(--radius)",
} as React.CSSProperties } as React.CSSProperties
} }
{...props} {...props}
/> />
); );
}; };
export { Toaster }; export { Toaster };
@@ -1,33 +1,33 @@
.card-spotlight { .card-spotlight {
position: relative; position: relative;
border-radius: 1.5rem; border-radius: 1.5rem;
border: 1px solid #222; border: 1px solid #222;
background-color: #111; background-color: #111;
padding: 2rem; padding: 2rem;
overflow: hidden; overflow: hidden;
--mouse-x: 50%; --mouse-x: 50%;
--mouse-y: 50%; --mouse-y: 50%;
--spotlight-color: rgba(255, 255, 255, 0.05); --spotlight-color: rgba(255, 255, 255, 0.05);
} }
.card-spotlight::before { .card-spotlight::before {
content: ""; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: radial-gradient( background: radial-gradient(
circle at var(--mouse-x) var(--mouse-y), circle at var(--mouse-x) var(--mouse-y),
var(--spotlight-color), var(--spotlight-color),
transparent 80% transparent 80%
); );
opacity: 0; opacity: 0;
transition: opacity 0.5s ease; transition: opacity 0.5s ease;
pointer-events: none; pointer-events: none;
} }
.card-spotlight:hover::before, .card-spotlight:hover::before,
.card-spotlight:focus-within::before { .card-spotlight:focus-within::before {
opacity: 0.6; opacity: 0.6;
} }
@@ -1,49 +1,49 @@
"use client"; "use client";
import React, { useRef } from "react"; import React, { useRef } from "react";
import "./spotlight-card.css"; import "./spotlight-card.css";
interface Position { interface Position {
x: number; x: number;
y: number; y: number;
} }
interface SpotlightCardProps extends React.PropsWithChildren { interface SpotlightCardProps extends React.PropsWithChildren {
className?: string; className?: string;
spotlightColor?: `rgba(${number}, ${number}, ${number}, ${number})`; spotlightColor?: `rgba(${number}, ${number}, ${number}, ${number})`;
style?: React.CSSProperties; style?: React.CSSProperties;
} }
const SpotlightCard: React.FC<SpotlightCardProps> = ({ const SpotlightCard: React.FC<SpotlightCardProps> = ({
style, style,
children, children,
className = "", className = "",
spotlightColor = "rgba(255, 255, 255, 0.25)", spotlightColor = "rgba(255, 255, 255, 0.25)",
}) => { }) => {
const divRef = useRef<HTMLDivElement>(null); const divRef = useRef<HTMLDivElement>(null);
const handleMouseMove: React.MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseMove: React.MouseEventHandler<HTMLDivElement> = (e) => {
if (!divRef.current) return; if (!divRef.current) return;
const rect = divRef.current.getBoundingClientRect(); const rect = divRef.current.getBoundingClientRect();
const x = e.clientX - rect.left; const x = e.clientX - rect.left;
const y = e.clientY - rect.top; const y = e.clientY - rect.top;
divRef.current.style.setProperty("--mouse-x", `${x}px`); divRef.current.style.setProperty("--mouse-x", `${x}px`);
divRef.current.style.setProperty("--mouse-y", `${y}px`); divRef.current.style.setProperty("--mouse-y", `${y}px`);
divRef.current.style.setProperty("--spotlight-color", spotlightColor); divRef.current.style.setProperty("--spotlight-color", spotlightColor);
}; };
return ( return (
<div <div
ref={divRef} ref={divRef}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
className={`card-spotlight ${className}`} className={`card-spotlight ${className}`}
style={style} style={style}
> >
{children} {children}
</div> </div>
); );
}; };
export default SpotlightCard; export default SpotlightCard;
+31 -31
View File
@@ -1,31 +1,31 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch"; import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Switch({ function Switch({
className, className,
...props ...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) { }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return ( return (
<SwitchPrimitive.Root <SwitchPrimitive.Root
data-slot="switch" data-slot="switch"
className={cn( 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-[#55A6FE] dark:data-[state=checked]:bg-[#0062D9] 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", "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-[#55A6FE] dark:data-[state=checked]:bg-[#0062D9] 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, className,
)} )}
{...props} {...props}
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
className={cn( 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", "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> </SwitchPrimitive.Root>
); );
} }
export { Switch }; export { Switch };
+109 -109
View File
@@ -1,109 +1,109 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs"; import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Tabs({ function Tabs({
className, className,
orientation = "horizontal", orientation = "horizontal",
...props ...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) { }: React.ComponentProps<typeof TabsPrimitive.Root>) {
return ( return (
<TabsPrimitive.Root <TabsPrimitive.Root
data-slot="tabs" data-slot="tabs"
data-orientation={orientation} data-orientation={orientation}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
className, className,
)} )}
{...props} {...props}
/> />
); );
} }
const tabsListVariants = cva( 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", "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: { variants: {
variant: { variant: {
default: default:
"bg-muted dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-hidden dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0", "bg-muted dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-hidden dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
line: line:
"gap-1 bg-transparent dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-visible dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0", "gap-1 bg-transparent dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-visible dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
}, },
); );
const TabsList = React.forwardRef< const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>, React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants> VariantProps<typeof tabsListVariants>
>(({ className, variant = "default", ...props }, ref) => { >(({ className, variant = "default", ...props }, ref) => {
return ( return (
<TabsPrimitive.List <TabsPrimitive.List
ref={ref} ref={ref}
data-slot="tabs-list" data-slot="tabs-list"
data-variant={variant} data-variant={variant}
className={cn(tabsListVariants({ variant }), className)} className={cn(tabsListVariants({ variant }), className)}
{...props} {...props}
/> />
); );
}); });
TabsList.displayName = TabsPrimitive.List.displayName; TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef< const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>, React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
return ( return (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
data-slot="tabs-trigger" data-slot="tabs-trigger"
className={cn( className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 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-[color,box-shadow] outline-none group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] 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", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 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-[color,box-shadow] outline-none group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] 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",
"dark:text-muted-foreground dark:hover:text-foreground", "dark:text-muted-foreground dark:hover:text-foreground",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background data-[state=active]:text-foreground", "data-[state=active]:bg-background data-[state=active]:text-foreground",
"dark:group-data-[variant=line]/tabs-list:relative dark:group-data-[variant=line]/tabs-list:!h-full dark:group-data-[variant=line]/tabs-list:self-stretch dark:group-data-[variant=line]/tabs-list:flex-none dark:group-data-[variant=line]/tabs-list:rounded-none dark:group-data-[variant=line]/tabs-list:border-0 dark:group-data-[variant=line]/tabs-list:bg-transparent dark:group-data-[variant=line]/tabs-list:px-4 dark:group-data-[variant=line]/tabs-list:!py-0 dark:group-data-[variant=line]/tabs-list:!leading-none dark:group-data-[variant=line]/tabs-list:text-[#CCD2DA] dark:group-data-[variant=line]/tabs-list:shadow-none dark:group-data-[variant=line]/tabs-list:hover:text-[#CCD2DA] dark:group-data-[variant=line]/tabs-list:focus-visible:ring-0 dark:group-data-[variant=line]/tabs-list:[&_svg]:text-[#CCD2DA]", "dark:group-data-[variant=line]/tabs-list:relative dark:group-data-[variant=line]/tabs-list:!h-full dark:group-data-[variant=line]/tabs-list:self-stretch dark:group-data-[variant=line]/tabs-list:flex-none dark:group-data-[variant=line]/tabs-list:rounded-none dark:group-data-[variant=line]/tabs-list:border-0 dark:group-data-[variant=line]/tabs-list:bg-transparent dark:group-data-[variant=line]/tabs-list:px-4 dark:group-data-[variant=line]/tabs-list:!py-0 dark:group-data-[variant=line]/tabs-list:!leading-none dark:group-data-[variant=line]/tabs-list:text-[#CCD2DA] dark:group-data-[variant=line]/tabs-list:shadow-none dark:group-data-[variant=line]/tabs-list:hover:text-[#CCD2DA] dark:group-data-[variant=line]/tabs-list:focus-visible:ring-0 dark:group-data-[variant=line]/tabs-list:[&_svg]:text-[#CCD2DA]",
"dark:group-data-[variant=line]/tabs-list:not(:first-child):border-l dark:group-data-[variant=line]/tabs-list:not(:first-child):border-[#1d3961] dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-l-0", "dark:group-data-[variant=line]/tabs-list:not(:first-child):border-l dark:group-data-[variant=line]/tabs-list:not(:first-child):border-[#1d3961] dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-l-0",
"dark:group-data-[variant=line]/tabs-list:data-[state=active]:rounded-none dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-0 dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-[#0062D9] dark:group-data-[variant=line]/tabs-list:data-[state=active]:text-[#FFFFFF] dark:group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=line]/tabs-list:data-[state=active]:[&_svg]:text-[#FFFFFF]", "dark:group-data-[variant=line]/tabs-list:data-[state=active]:rounded-none dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-0 dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-[#0062D9] dark:group-data-[variant=line]/tabs-list:data-[state=active]:text-[#FFFFFF] dark:group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=line]/tabs-list:data-[state=active]:[&_svg]:text-[#FFFFFF]",
"dark:group-data-[variant=default]/tabs-list:flex dark:group-data-[variant=default]/tabs-list:min-h-0 dark:group-data-[variant=default]/tabs-list:!h-full dark:group-data-[variant=default]/tabs-list:flex-1 dark:group-data-[variant=default]/tabs-list:self-stretch dark:group-data-[variant=default]/tabs-list:rounded-none dark:group-data-[variant=default]/tabs-list:border-0 dark:group-data-[variant=default]/tabs-list:bg-transparent dark:group-data-[variant=default]/tabs-list:px-4 dark:group-data-[variant=default]/tabs-list:!py-0 dark:group-data-[variant=default]/tabs-list:!leading-none dark:group-data-[variant=default]/tabs-list:text-[#CCD2DA] dark:group-data-[variant=default]/tabs-list:shadow-none dark:group-data-[variant=default]/tabs-list:hover:text-[#CCD2DA] dark:group-data-[variant=default]/tabs-list:focus-visible:ring-0 dark:group-data-[variant=default]/tabs-list:[&_svg]:text-[#CCD2DA]", "dark:group-data-[variant=default]/tabs-list:flex dark:group-data-[variant=default]/tabs-list:min-h-0 dark:group-data-[variant=default]/tabs-list:!h-full dark:group-data-[variant=default]/tabs-list:flex-1 dark:group-data-[variant=default]/tabs-list:self-stretch dark:group-data-[variant=default]/tabs-list:rounded-none dark:group-data-[variant=default]/tabs-list:border-0 dark:group-data-[variant=default]/tabs-list:bg-transparent dark:group-data-[variant=default]/tabs-list:px-4 dark:group-data-[variant=default]/tabs-list:!py-0 dark:group-data-[variant=default]/tabs-list:!leading-none dark:group-data-[variant=default]/tabs-list:text-[#CCD2DA] dark:group-data-[variant=default]/tabs-list:shadow-none dark:group-data-[variant=default]/tabs-list:hover:text-[#CCD2DA] dark:group-data-[variant=default]/tabs-list:focus-visible:ring-0 dark:group-data-[variant=default]/tabs-list:[&_svg]:text-[#CCD2DA]",
"dark:group-data-[variant=default]/tabs-list:not(:first-child):border-l dark:group-data-[variant=default]/tabs-list:not(:first-child):border-[#1d3961] dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-l-0", "dark:group-data-[variant=default]/tabs-list:not(:first-child):border-l dark:group-data-[variant=default]/tabs-list:not(:first-child):border-[#1d3961] dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-l-0",
"dark:group-data-[variant=default]/tabs-list:data-[state=active]:rounded-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-0 dark:group-data-[variant=default]/tabs-list:data-[state=active]:bg-[#0062D9] dark:group-data-[variant=default]/tabs-list:data-[state=active]:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:hover:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:data-[state=active]:[&_svg]:text-[#FFFFFF]", "dark:group-data-[variant=default]/tabs-list:data-[state=active]:rounded-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-0 dark:group-data-[variant=default]/tabs-list:data-[state=active]:bg-[#0062D9] dark:group-data-[variant=default]/tabs-list:data-[state=active]:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:hover:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:data-[state=active]:[&_svg]:text-[#FFFFFF]",
"group-data-[variant=line]/tabs-list:after:pointer-events-none group-data-[variant=line]/tabs-list:after:absolute group-data-[variant=line]/tabs-list:after:inset-x-2 group-data-[variant=line]/tabs-list:after:bottom-[-5px] group-data-[variant=line]/tabs-list:after:h-0.5 group-data-[variant=line]/tabs-list:after:bg-foreground group-data-[variant=line]/tabs-list:after:opacity-0 group-data-[variant=line]/tabs-list:after:transition-opacity group-data-[variant=line]/tabs-list:after:content-[''] group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", "group-data-[variant=line]/tabs-list:after:pointer-events-none group-data-[variant=line]/tabs-list:after:absolute group-data-[variant=line]/tabs-list:after:inset-x-2 group-data-[variant=line]/tabs-list:after:bottom-[-5px] group-data-[variant=line]/tabs-list:after:h-0.5 group-data-[variant=line]/tabs-list:after:bg-foreground group-data-[variant=line]/tabs-list:after:opacity-0 group-data-[variant=line]/tabs-list:after:transition-opacity group-data-[variant=line]/tabs-list:after:content-[''] group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
"dark:group-data-[variant=line]/tabs-list:after:inset-x-0 dark:group-data-[variant=line]/tabs-list:after:bottom-0 dark:group-data-[variant=line]/tabs-list:after:z-[2] dark:group-data-[variant=line]/tabs-list:after:h-[2px] dark:group-data-[variant=line]/tabs-list:after:bg-[#FFFFFF]", "dark:group-data-[variant=line]/tabs-list:after:inset-x-0 dark:group-data-[variant=line]/tabs-list:after:bottom-0 dark:group-data-[variant=line]/tabs-list:after:z-[2] dark:group-data-[variant=line]/tabs-list:after:h-[2px] dark:group-data-[variant=line]/tabs-list:after:bg-[#FFFFFF]",
className, className,
)} )}
{...props} {...props}
/> />
); );
}); });
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef< const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>, React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
return ( return (
<TabsPrimitive.Content <TabsPrimitive.Content
ref={ref} ref={ref}
data-slot="tabs-content" data-slot="tabs-content"
className={cn("flex-1 outline-none", className)} className={cn("flex-1 outline-none", className)}
{...props} {...props}
/> />
); );
}); });
TabsContent.displayName = TabsPrimitive.Content.displayName; TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }; export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
+257 -257
View File
@@ -1,257 +1,257 @@
"use client"; "use client";
import { import {
Children, Children,
createContext, createContext,
useContext, useContext,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { motion, type MotionProps, useInView } from "motion/react"; import { motion, type MotionProps, useInView } from "motion/react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface SequenceContextValue { interface SequenceContextValue {
completeItem: (index: number) => void; completeItem: (index: number) => void;
activeIndex: number; activeIndex: number;
sequenceStarted: boolean; sequenceStarted: boolean;
} }
const SequenceContext = createContext<SequenceContextValue | null>(null); const SequenceContext = createContext<SequenceContextValue | null>(null);
const useSequence = () => useContext(SequenceContext); const useSequence = () => useContext(SequenceContext);
const ItemIndexContext = createContext<number | null>(null); const ItemIndexContext = createContext<number | null>(null);
const useItemIndex = () => useContext(ItemIndexContext); const useItemIndex = () => useContext(ItemIndexContext);
interface AnimatedSpanProps extends MotionProps { interface AnimatedSpanProps extends MotionProps {
children: React.ReactNode; children: React.ReactNode;
delay?: number; delay?: number;
className?: string; className?: string;
startOnView?: boolean; startOnView?: boolean;
} }
export const AnimatedSpan = ({ export const AnimatedSpan = ({
children, children,
delay = 0, delay = 0,
className, className,
startOnView = false, startOnView = false,
...props ...props
}: AnimatedSpanProps) => { }: AnimatedSpanProps) => {
const elementRef = useRef<HTMLDivElement | null>(null); const elementRef = useRef<HTMLDivElement | null>(null);
const isInView = useInView(elementRef as React.RefObject<Element>, { const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3, amount: 0.3,
once: true, once: true,
}); });
const sequence = useSequence(); const sequence = useSequence();
const itemIndex = useItemIndex(); const itemIndex = useItemIndex();
const [hasStarted, setHasStarted] = useState(false); const [hasStarted, setHasStarted] = useState(false);
useEffect(() => { useEffect(() => {
if (!sequence || itemIndex === null) return; if (!sequence || itemIndex === null) return;
if (!sequence.sequenceStarted) return; if (!sequence.sequenceStarted) return;
if (hasStarted) return; if (hasStarted) return;
if (sequence.activeIndex === itemIndex) { if (sequence.activeIndex === itemIndex) {
setHasStarted(true); setHasStarted(true);
} }
}, [sequence?.activeIndex, sequence?.sequenceStarted, hasStarted, itemIndex]); }, [sequence?.activeIndex, sequence?.sequenceStarted, hasStarted, itemIndex]);
const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true; const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true;
return ( return (
<motion.div <motion.div
ref={elementRef} ref={elementRef}
initial={{ opacity: 0, y: -5 }} initial={{ opacity: 0, y: -5 }}
animate={shouldAnimate ? { opacity: 1, y: 0 } : { opacity: 0, y: -5 }} animate={shouldAnimate ? { opacity: 1, y: 0 } : { opacity: 0, y: -5 }}
transition={{ duration: 0.3, delay: sequence ? 0 : delay / 1000 }} transition={{ duration: 0.3, delay: sequence ? 0 : delay / 1000 }}
className={cn("grid text-sm font-normal tracking-tight", className)} className={cn("grid text-sm font-normal tracking-tight", className)}
onAnimationComplete={() => { onAnimationComplete={() => {
if (!sequence) return; if (!sequence) return;
if (itemIndex === null) return; if (itemIndex === null) return;
sequence.completeItem(itemIndex); sequence.completeItem(itemIndex);
}} }}
{...props} {...props}
> >
{children} {children}
</motion.div> </motion.div>
); );
}; };
interface TypingAnimationProps extends MotionProps { interface TypingAnimationProps extends MotionProps {
children: string; children: string;
className?: string; className?: string;
duration?: number; duration?: number;
delay?: number; delay?: number;
as?: React.ElementType; as?: React.ElementType;
startOnView?: boolean; startOnView?: boolean;
} }
export const TypingAnimation = ({ export const TypingAnimation = ({
children, children,
className, className,
duration = 60, duration = 60,
delay = 0, delay = 0,
as: Component = "span", as: Component = "span",
startOnView = true, startOnView = true,
...props ...props
}: TypingAnimationProps) => { }: TypingAnimationProps) => {
if (typeof children !== "string") { if (typeof children !== "string") {
throw new Error("TypingAnimation: children must be a string. Received:"); throw new Error("TypingAnimation: children must be a string. Received:");
} }
const MotionComponent = useMemo( const MotionComponent = useMemo(
() => () =>
motion.create(Component, { motion.create(Component, {
forwardMotionProps: true, forwardMotionProps: true,
}), }),
[Component], [Component],
); );
const [displayedText, setDisplayedText] = useState<string>(""); const [displayedText, setDisplayedText] = useState<string>("");
const [started, setStarted] = useState(false); const [started, setStarted] = useState(false);
const elementRef = useRef<HTMLElement | null>(null); const elementRef = useRef<HTMLElement | null>(null);
const isInView = useInView(elementRef as React.RefObject<Element>, { const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3, amount: 0.3,
once: true, once: true,
}); });
const sequence = useSequence(); const sequence = useSequence();
const itemIndex = useItemIndex(); const itemIndex = useItemIndex();
useEffect(() => { useEffect(() => {
if (sequence && itemIndex !== null) { if (sequence && itemIndex !== null) {
if (!sequence.sequenceStarted) return; if (!sequence.sequenceStarted) return;
if (started) return; if (started) return;
if (sequence.activeIndex === itemIndex) { if (sequence.activeIndex === itemIndex) {
setStarted(true); setStarted(true);
} }
return; return;
} }
if (!startOnView) { if (!startOnView) {
const startTimeout = setTimeout(() => setStarted(true), delay); const startTimeout = setTimeout(() => setStarted(true), delay);
return () => clearTimeout(startTimeout); return () => clearTimeout(startTimeout);
} }
if (!isInView) return; if (!isInView) return;
const startTimeout = setTimeout(() => setStarted(true), delay); const startTimeout = setTimeout(() => setStarted(true), delay);
return () => clearTimeout(startTimeout); return () => clearTimeout(startTimeout);
}, [ }, [
delay, delay,
startOnView, startOnView,
isInView, isInView,
started, started,
sequence?.activeIndex, sequence?.activeIndex,
sequence?.sequenceStarted, sequence?.sequenceStarted,
itemIndex, itemIndex,
]); ]);
useEffect(() => { useEffect(() => {
if (!started) return; if (!started) return;
let i = 0; let i = 0;
const typingEffect = setInterval(() => { const typingEffect = setInterval(() => {
if (i < children.length) { if (i < children.length) {
setDisplayedText(children.substring(0, i + 1)); setDisplayedText(children.substring(0, i + 1));
i++; i++;
} else { } else {
clearInterval(typingEffect); clearInterval(typingEffect);
if (sequence && itemIndex !== null) { if (sequence && itemIndex !== null) {
sequence.completeItem(itemIndex); sequence.completeItem(itemIndex);
} }
} }
}, duration); }, duration);
return () => { return () => {
clearInterval(typingEffect); clearInterval(typingEffect);
}; };
}, [children, duration, started]); }, [children, duration, started]);
return ( return (
<MotionComponent <MotionComponent
ref={elementRef} ref={elementRef}
className={cn("text-sm font-normal tracking-tight", className)} className={cn("text-sm font-normal tracking-tight", className)}
{...props} {...props}
> >
{displayedText} {displayedText}
</MotionComponent> </MotionComponent>
); );
}; };
interface TerminalProps { interface TerminalProps {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
sequence?: boolean; sequence?: boolean;
startOnView?: boolean; startOnView?: boolean;
} }
export const Terminal = ({ export const Terminal = ({
children, children,
className, className,
sequence = true, sequence = true,
startOnView = true, startOnView = true,
}: TerminalProps) => { }: TerminalProps) => {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const isInView = useInView(containerRef as React.RefObject<Element>, { const isInView = useInView(containerRef as React.RefObject<Element>, {
amount: 0.3, amount: 0.3,
once: true, once: true,
}); });
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
const sequenceHasStarted = sequence ? !startOnView || isInView : false; const sequenceHasStarted = sequence ? !startOnView || isInView : false;
const contextValue = useMemo<SequenceContextValue | null>(() => { const contextValue = useMemo<SequenceContextValue | null>(() => {
if (!sequence) return null; if (!sequence) return null;
return { return {
completeItem: (index: number) => { completeItem: (index: number) => {
setActiveIndex((current) => setActiveIndex((current) =>
index === current ? current + 1 : current, index === current ? current + 1 : current,
); );
}, },
activeIndex, activeIndex,
sequenceStarted: sequenceHasStarted, sequenceStarted: sequenceHasStarted,
}; };
}, [sequence, activeIndex, sequenceHasStarted]); }, [sequence, activeIndex, sequenceHasStarted]);
const wrappedChildren = useMemo(() => { const wrappedChildren = useMemo(() => {
if (!sequence) return children; if (!sequence) return children;
const array = Children.toArray(children); const array = Children.toArray(children);
return array.map((child, index) => ( return array.map((child, index) => (
<ItemIndexContext.Provider key={index} value={index}> <ItemIndexContext.Provider key={index} value={index}>
{child as React.ReactNode} {child as React.ReactNode}
</ItemIndexContext.Provider> </ItemIndexContext.Provider>
)); ));
}, [children, sequence]); }, [children, sequence]);
const content = ( const content = (
<div <div
ref={containerRef} ref={containerRef}
className={cn( className={cn(
"border-border bg-background/25 z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border", "border-border bg-background/25 z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border",
className, className,
)} )}
> >
<div className="border-border flex flex-col gap-y-2 border-b p-4"> <div className="border-border flex flex-col gap-y-2 border-b p-4">
<div className="flex flex-row gap-x-2"> <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-red-500"></div>
<div className="h-2 w-2 rounded-full bg-yellow-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 className="h-2 w-2 rounded-full bg-green-500"></div>
</div> </div>
</div> </div>
<pre className="p-4"> <pre className="p-4">
<code className="grid gap-y-1 overflow-auto">{wrappedChildren}</code> <code className="grid gap-y-1 overflow-auto">{wrappedChildren}</code>
</pre> </pre>
</div> </div>
); );
if (!sequence) return content; if (!sequence) return content;
return ( return (
<SequenceContext.Provider value={contextValue}> <SequenceContext.Provider value={contextValue}>
{content} {content}
</SequenceContext.Provider> </SequenceContext.Provider>
); );
}; };
+10 -10
View File
@@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const Textarea = React.forwardRef< const Textarea = React.forwardRef<
HTMLTextAreaElement, HTMLTextAreaElement,
React.ComponentProps<"textarea"> React.ComponentProps<"textarea">
@@ -15,15 +15,15 @@ const Textarea = React.forwardRef<
rows={rows ?? 3} rows={rows ?? 3}
className={cn( className={cn(
"border-input placeholder:text-muted-foreground/60 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "border-input placeholder:text-muted-foreground/60 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] ", "dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] ",
"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", "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, className,
)} )}
{...props} {...props}
/> />
); );
}); });
Textarea.displayName = "Textarea"; Textarea.displayName = "Textarea";
export { Textarea }; export { Textarea };
+83 -83
View File
@@ -1,83 +1,83 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority"; import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle"; import { toggleVariants } from "@/components/ui/toggle";
const ToggleGroupContext = React.createContext< const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & { VariantProps<typeof toggleVariants> & {
spacing?: number; spacing?: number;
} }
>({ >({
size: "default", size: "default",
variant: "default", variant: "default",
spacing: 0, spacing: 0,
}); });
function ToggleGroup({ function ToggleGroup({
className, className,
variant, variant,
size, size,
spacing = 0, spacing = 0,
children, children,
...props ...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> & }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants> & { VariantProps<typeof toggleVariants> & {
spacing?: number; spacing?: number;
}) { }) {
return ( return (
<ToggleGroupPrimitive.Root <ToggleGroupPrimitive.Root
data-slot="toggle-group" data-slot="toggle-group"
data-variant={variant} data-variant={variant}
data-size={size} data-size={size}
data-spacing={spacing} data-spacing={spacing}
style={{ "--gap": spacing } as React.CSSProperties} style={{ "--gap": spacing } as React.CSSProperties}
className={cn( className={cn(
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs", "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
className, className,
)} )}
{...props} {...props}
> >
<ToggleGroupContext.Provider value={{ variant, size, spacing }}> <ToggleGroupContext.Provider value={{ variant, size, spacing }}>
{children} {children}
</ToggleGroupContext.Provider> </ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root> </ToggleGroupPrimitive.Root>
); );
} }
function ToggleGroupItem({ function ToggleGroupItem({
className, className,
children, children,
variant, variant,
size, size,
...props ...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) { VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext); const context = React.useContext(ToggleGroupContext);
return ( return (
<ToggleGroupPrimitive.Item <ToggleGroupPrimitive.Item
data-slot="toggle-group-item" data-slot="toggle-group-item"
data-variant={context.variant || variant} data-variant={context.variant || variant}
data-size={context.size || size} data-size={context.size || size}
data-spacing={context.spacing} data-spacing={context.spacing}
className={cn( className={cn(
toggleVariants({ toggleVariants({
variant: context.variant || variant, variant: context.variant || variant,
size: context.size || size, size: context.size || size,
}), }),
"w-auto min-w-0 shrink-0 cursor-pointer px-3 focus:z-10 focus-visible:z-10", "w-auto min-w-0 shrink-0 cursor-pointer px-3 focus:z-10 focus-visible:z-10",
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l", "data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
</ToggleGroupPrimitive.Item> </ToggleGroupPrimitive.Item>
); );
} }
export { ToggleGroup, ToggleGroupItem }; export { ToggleGroup, ToggleGroupItem };
+47 -47
View File
@@ -1,47 +1,47 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle"; import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const toggleVariants = cva( const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-transparent", default: "bg-transparent",
outline: outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
}, },
size: { size: {
default: "h-9 px-2 min-w-9", default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8", sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10", lg: "h-10 px-2.5 min-w-10",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default", size: "default",
}, },
}, },
); );
function Toggle({ function Toggle({
className, className,
variant, variant,
size, size,
...props ...props
}: React.ComponentProps<typeof TogglePrimitive.Root> & }: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) { VariantProps<typeof toggleVariants>) {
return ( return (
<TogglePrimitive.Root <TogglePrimitive.Root
data-slot="toggle" data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))} className={cn(toggleVariants({ variant, size, className }))}
{...props} {...props}
/> />
); );
} }
export { Toggle, toggleVariants }; export { Toggle, toggleVariants };
+32 -32
View File
@@ -1,33 +1,33 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function TooltipProvider({ function TooltipProvider({
delayDuration = 0, delayDuration = 0,
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return ( return (
<TooltipPrimitive.Provider <TooltipPrimitive.Provider
data-slot="tooltip-provider" data-slot="tooltip-provider"
delayDuration={delayDuration} delayDuration={delayDuration}
{...props} {...props}
/> />
); );
} }
function Tooltip({ function Tooltip({
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) { }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return ( return (
<TooltipProvider> <TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} /> <TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider> </TooltipProvider>
); );
} }
const TooltipTrigger = React.forwardRef< const TooltipTrigger = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Trigger>, React.ElementRef<typeof TooltipPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>
@@ -66,5 +66,5 @@ const TooltipContent = React.forwardRef<
}); });
TooltipContent.displayName = TooltipPrimitive.Content.displayName; TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+53 -53
View File
@@ -1,53 +1,53 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AnimatePresence, motion, type MotionProps } from "motion/react"; import { AnimatePresence, motion, type MotionProps } from "motion/react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { AuroraText } from "./aurora-text"; import { AuroraText } from "./aurora-text";
interface WordRotateProps { interface WordRotateProps {
words: string[]; words: string[];
duration?: number; duration?: number;
motionProps?: MotionProps; motionProps?: MotionProps;
className?: string; className?: string;
} }
export function WordRotate({ export function WordRotate({
words, words,
duration = 2200, duration = 2200,
motionProps = { motionProps = {
initial: { opacity: 0, y: -50, filter: "blur(16px)" }, initial: { opacity: 0, y: -50, filter: "blur(16px)" },
animate: { opacity: 1, y: 0, filter: "blur(0px)" }, animate: { opacity: 1, y: 0, filter: "blur(0px)" },
exit: { opacity: 0, y: 50, filter: "blur(16px)" }, exit: { opacity: 0, y: 50, filter: "blur(16px)" },
transition: { duration: 0.3, ease: "easeOut" }, transition: { duration: 0.3, ease: "easeOut" },
}, },
className, className,
}: WordRotateProps) { }: WordRotateProps) {
const [index, setIndex] = useState(0); const [index, setIndex] = useState(0);
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
setIndex((prevIndex) => (prevIndex + 1) % words.length); setIndex((prevIndex) => (prevIndex + 1) % words.length);
}, duration); }, duration);
// Clean up interval on unmount // Clean up interval on unmount
return () => clearInterval(interval); return () => clearInterval(interval);
}, [words, duration]); }, [words, duration]);
return ( return (
<div className="overflow-hidden py-2"> <div className="overflow-hidden py-2">
<AnimatePresence mode="popLayout"> <AnimatePresence mode="popLayout">
<motion.h1 <motion.h1
key={words[index]} key={words[index]}
className={cn(className)} className={cn(className)}
{...motionProps} {...motionProps}
> >
<AuroraText speed={3} colors={["#efefbb", "#e9c665", "#e3a812"]}> <AuroraText speed={3} colors={["#efefbb", "#e9c665", "#e3a812"]}>
{words[index]} {words[index]}
</AuroraText> </AuroraText>
</motion.h1> </motion.h1>
</AnimatePresence> </AnimatePresence>
</div> </div>
); );
} }
Loaded 100 of 864 files, more files were not shown because too many files have changed in this diff. Show more