mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-04-25 11:18:22 +00:00
ci: enforce code formatting checks for backend and frontend (#1536)
This commit is contained in:
parent
06a623f9c8
commit
084dc7e748
5
.github/workflows/lint-check.yml
vendored
5
.github/workflows/lint-check.yml
vendored
@ -53,6 +53,11 @@ jobs:
|
||||
cd frontend
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Check frontend formatting
|
||||
run: |
|
||||
cd frontend
|
||||
pnpm format
|
||||
|
||||
- name: Run frontend linting
|
||||
run: |
|
||||
cd frontend
|
||||
|
||||
@ -12,6 +12,7 @@ test:
|
||||
|
||||
lint:
|
||||
uvx ruff check .
|
||||
uvx ruff format --check .
|
||||
|
||||
format:
|
||||
uvx ruff check . --fix && uvx ruff format .
|
||||
|
||||
@ -66,14 +66,9 @@ def _normalize_custom_agent_name(raw_value: str) -> str:
|
||||
"""Normalize legacy channel assistant IDs into valid custom agent names."""
|
||||
normalized = raw_value.strip().lower().replace("_", "-")
|
||||
if not normalized:
|
||||
raise InvalidChannelSessionConfigError(
|
||||
"Channel session assistant_id is empty. Use 'lead_agent' or a valid custom agent name."
|
||||
)
|
||||
raise InvalidChannelSessionConfigError("Channel session assistant_id is empty. Use 'lead_agent' or a valid custom agent name.")
|
||||
if not CUSTOM_AGENT_NAME_PATTERN.fullmatch(normalized):
|
||||
raise InvalidChannelSessionConfigError(
|
||||
f"Invalid channel session assistant_id {raw_value!r}. "
|
||||
"Use 'lead_agent' or a custom agent name containing only letters, digits, and hyphens."
|
||||
)
|
||||
raise InvalidChannelSessionConfigError(f"Invalid channel session assistant_id {raw_value!r}. Use 'lead_agent' or a custom agent name containing only letters, digits, and hyphens.")
|
||||
return normalized
|
||||
|
||||
|
||||
|
||||
@ -48,9 +48,7 @@ def _make_file_sandbox_writable(file_path: os.PathLike[str] | str) -> None:
|
||||
logger.warning("Skipping sandbox chmod for symlinked upload path: %s", file_path)
|
||||
return
|
||||
|
||||
writable_mode = (
|
||||
stat.S_IMODE(file_stat.st_mode) | stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
|
||||
)
|
||||
writable_mode = stat.S_IMODE(file_stat.st_mode) | stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
|
||||
chmod_kwargs = {"follow_symlinks": False} if os.chmod in os.supports_follow_symlinks else {}
|
||||
os.chmod(file_path, writable_mode, **chmod_kwargs)
|
||||
|
||||
|
||||
@ -71,9 +71,7 @@ class FileMemoryStorage(MemoryStorage):
|
||||
if not agent_name:
|
||||
raise ValueError("Agent name must be a non-empty string.")
|
||||
if not AGENT_NAME_PATTERN.match(agent_name):
|
||||
raise ValueError(
|
||||
f"Invalid agent name {agent_name!r}: names must match {AGENT_NAME_PATTERN.pattern}"
|
||||
)
|
||||
raise ValueError(f"Invalid agent name {agent_name!r}: names must match {AGENT_NAME_PATTERN.pattern}")
|
||||
|
||||
def _get_memory_file_path(self, agent_name: str | None = None) -> Path:
|
||||
"""Get the path to the memory file."""
|
||||
@ -180,18 +178,15 @@ def get_memory_storage() -> MemoryStorage:
|
||||
try:
|
||||
module_path, class_name = storage_class_path.rsplit(".", 1)
|
||||
import importlib
|
||||
|
||||
module = importlib.import_module(module_path)
|
||||
storage_class = getattr(module, class_name)
|
||||
|
||||
# Validate that the configured storage is a MemoryStorage implementation
|
||||
if not isinstance(storage_class, type):
|
||||
raise TypeError(
|
||||
f"Configured memory storage '{storage_class_path}' is not a class: {storage_class!r}"
|
||||
)
|
||||
raise TypeError(f"Configured memory storage '{storage_class_path}' is not a class: {storage_class!r}")
|
||||
if not issubclass(storage_class, MemoryStorage):
|
||||
raise TypeError(
|
||||
f"Configured memory storage '{storage_class_path}' is not a subclass of MemoryStorage"
|
||||
)
|
||||
raise TypeError(f"Configured memory storage '{storage_class_path}' is not a subclass of MemoryStorage")
|
||||
|
||||
_storage_instance = storage_class()
|
||||
except Exception as e:
|
||||
|
||||
@ -27,10 +27,12 @@ def _save_memory_to_file(memory_data: dict[str, Any], agent_name: str | None = N
|
||||
"""Backward-compatible wrapper around the configured memory storage save path."""
|
||||
return get_memory_storage().save(memory_data, agent_name)
|
||||
|
||||
|
||||
def get_memory_data(agent_name: str | None = None) -> dict[str, Any]:
|
||||
"""Get the current memory data via storage provider."""
|
||||
return get_memory_storage().load(agent_name)
|
||||
|
||||
|
||||
def reload_memory_data(agent_name: str | None = None) -> dict[str, Any]:
|
||||
"""Reload memory data via storage provider."""
|
||||
return get_memory_storage().reload(agent_name)
|
||||
|
||||
@ -162,10 +162,7 @@ class ClaudeChatModel(ChatAnthropic):
|
||||
system = payload.get("system")
|
||||
if isinstance(system, list):
|
||||
# Remove any existing billing blocks, then insert a single one at index 0.
|
||||
filtered = [
|
||||
b for b in system
|
||||
if not (isinstance(b, dict) and OAUTH_BILLING_HEADER in b.get("text", ""))
|
||||
]
|
||||
filtered = [b for b in system if not (isinstance(b, dict) and OAUTH_BILLING_HEADER in b.get("text", ""))]
|
||||
payload["system"] = [billing_block] + filtered
|
||||
elif isinstance(system, str):
|
||||
if OAUTH_BILLING_HEADER in system:
|
||||
@ -183,11 +180,13 @@ class ClaudeChatModel(ChatAnthropic):
|
||||
hostname = socket.gethostname()
|
||||
device_id = hashlib.sha256(f"deerflow-{hostname}".encode()).hexdigest()
|
||||
session_id = str(uuid.uuid4())
|
||||
payload["metadata"]["user_id"] = json.dumps({
|
||||
"device_id": device_id,
|
||||
"account_uuid": "deerflow",
|
||||
"session_id": session_id,
|
||||
})
|
||||
payload["metadata"]["user_id"] = json.dumps(
|
||||
{
|
||||
"device_id": device_id,
|
||||
"account_uuid": "deerflow",
|
||||
"session_id": session_id,
|
||||
}
|
||||
)
|
||||
|
||||
def _apply_prompt_caching(self, payload: dict) -> None:
|
||||
"""Apply ephemeral cache_control to system and recent messages."""
|
||||
|
||||
@ -84,9 +84,7 @@ class PatchedChatOpenAI(ChatOpenAI):
|
||||
else:
|
||||
# Fallback: match assistant-role entries positionally against AIMessages.
|
||||
ai_messages = [m for m in original_messages if isinstance(m, AIMessage)]
|
||||
assistant_payloads = [
|
||||
(i, m) for i, m in enumerate(payload_messages) if m.get("role") == "assistant"
|
||||
]
|
||||
assistant_payloads = [(i, m) for i, m in enumerate(payload_messages) if m.get("role") == "assistant"]
|
||||
for (_, payload_msg), ai_msg in zip(assistant_payloads, ai_messages):
|
||||
_restore_tool_call_signatures(payload_msg, ai_msg)
|
||||
|
||||
|
||||
@ -100,7 +100,7 @@ def _resolve_skills_path(path: str) -> str:
|
||||
if path == skills_container:
|
||||
return skills_host
|
||||
|
||||
relative = path[len(skills_container):].lstrip("/")
|
||||
relative = path[len(skills_container) :].lstrip("/")
|
||||
return _join_path_preserving_style(skills_host, relative)
|
||||
|
||||
|
||||
|
||||
@ -197,6 +197,7 @@ async def task_tool(
|
||||
writer({"type": "task_timed_out", "task_id": task_id})
|
||||
return f"Task polling timed out after {timeout_minutes} minutes. This may indicate the background task is stuck. Status: {result.status.value}"
|
||||
except asyncio.CancelledError:
|
||||
|
||||
async def cleanup_when_done() -> None:
|
||||
max_cleanup_polls = max_poll_count
|
||||
cleanup_poll_count = 0
|
||||
@ -211,9 +212,7 @@ async def task_tool(
|
||||
return
|
||||
|
||||
if cleanup_poll_count > max_cleanup_polls:
|
||||
logger.warning(
|
||||
f"[trace={trace_id}] Deferred cleanup for task {task_id} timed out after {cleanup_poll_count} polls"
|
||||
)
|
||||
logger.warning(f"[trace={trace_id}] Deferred cleanup for task {task_id} timed out after {cleanup_poll_count} polls")
|
||||
return
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
@ -118,9 +118,7 @@ def _regex_score(pattern: str, entry: DeferredToolEntry) -> int:
|
||||
# loop.run_in_executor, Python copies the current context to the worker thread,
|
||||
# so the ContextVar value is correctly inherited there too.
|
||||
|
||||
_registry_var: contextvars.ContextVar[DeferredToolRegistry | None] = contextvars.ContextVar(
|
||||
"deferred_tool_registry", default=None
|
||||
)
|
||||
_registry_var: contextvars.ContextVar[DeferredToolRegistry | None] = contextvars.ContextVar("deferred_tool_registry", default=None)
|
||||
|
||||
|
||||
def get_deferred_registry() -> DeferredToolRegistry | None:
|
||||
|
||||
@ -600,10 +600,7 @@ class TestChannelManager:
|
||||
await manager.stop()
|
||||
|
||||
mock_client.runs.wait.assert_not_called()
|
||||
assert outbound_received[0].text == (
|
||||
"Invalid channel session assistant_id 'bad agent!'. "
|
||||
"Use 'lead_agent' or a custom agent name containing only letters, digits, and hyphens."
|
||||
)
|
||||
assert outbound_received[0].text == ("Invalid channel session assistant_id 'bad agent!'. Use 'lead_agent' or a custom agent name containing only letters, digits, and hyphens.")
|
||||
|
||||
_run(go())
|
||||
|
||||
|
||||
@ -56,10 +56,7 @@ def test_billing_not_duplicated_on_second_call(model):
|
||||
payload = {"system": [{"type": "text", "text": "prompt"}]}
|
||||
model._apply_oauth_billing(payload)
|
||||
model._apply_oauth_billing(payload)
|
||||
billing_count = sum(
|
||||
1 for b in payload["system"]
|
||||
if isinstance(b, dict) and OAUTH_BILLING_HEADER in b.get("text", "")
|
||||
)
|
||||
billing_count = sum(1 for b in payload["system"] if isinstance(b, dict) and OAUTH_BILLING_HEADER in b.get("text", ""))
|
||||
assert billing_count == 1
|
||||
|
||||
|
||||
|
||||
@ -65,14 +65,7 @@ class TestClientInit:
|
||||
def test_custom_params(self, mock_app_config):
|
||||
mock_middleware = MagicMock()
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
c = DeerFlowClient(
|
||||
model_name="gpt-4",
|
||||
thinking_enabled=False,
|
||||
subagent_enabled=True,
|
||||
plan_mode=True,
|
||||
agent_name="test-agent",
|
||||
middlewares=[mock_middleware]
|
||||
)
|
||||
c = DeerFlowClient(model_name="gpt-4", thinking_enabled=False, subagent_enabled=True, plan_mode=True, agent_name="test-agent", middlewares=[mock_middleware])
|
||||
assert c._model_name == "gpt-4"
|
||||
assert c._thinking_enabled is False
|
||||
assert c._subagent_enabled is True
|
||||
|
||||
@ -132,18 +132,13 @@ def test_build_middlewares_uses_resolved_model_name_for_vision(monkeypatch):
|
||||
monkeypatch.setattr(lead_agent_module, "_create_summarization_middleware", lambda: None)
|
||||
monkeypatch.setattr(lead_agent_module, "_create_todo_list_middleware", lambda is_plan_mode: None)
|
||||
|
||||
middlewares = lead_agent_module._build_middlewares(
|
||||
{"configurable": {"model_name": "stale-model", "is_plan_mode": False, "subagent_enabled": False}},
|
||||
model_name="vision-model",
|
||||
custom_middlewares=[MagicMock()]
|
||||
)
|
||||
middlewares = lead_agent_module._build_middlewares({"configurable": {"model_name": "stale-model", "is_plan_mode": False, "subagent_enabled": False}}, model_name="vision-model", custom_middlewares=[MagicMock()])
|
||||
|
||||
assert any(isinstance(m, lead_agent_module.ViewImageMiddleware) for m in middlewares)
|
||||
# verify the custom middleware is injected correctly
|
||||
assert len(middlewares) > 0 and isinstance(middlewares[-2], MagicMock)
|
||||
|
||||
|
||||
|
||||
def test_create_summarization_middleware_uses_configured_model_alias(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
lead_agent_module,
|
||||
|
||||
@ -33,6 +33,7 @@ class TestMemoryStorageInterface:
|
||||
|
||||
def test_abstract_methods(self):
|
||||
"""Should raise TypeError when trying to instantiate abstract class."""
|
||||
|
||||
class TestStorage(MemoryStorage):
|
||||
pass
|
||||
|
||||
@ -45,6 +46,7 @@ class TestFileMemoryStorage:
|
||||
|
||||
def test_get_memory_file_path_global(self, tmp_path):
|
||||
"""Should return global memory file path when agent_name is None."""
|
||||
|
||||
def mock_get_paths():
|
||||
mock_paths = MagicMock()
|
||||
mock_paths.memory_file = tmp_path / "memory.json"
|
||||
@ -58,6 +60,7 @@ class TestFileMemoryStorage:
|
||||
|
||||
def test_get_memory_file_path_agent(self, tmp_path):
|
||||
"""Should return per-agent memory file path when agent_name is provided."""
|
||||
|
||||
def mock_get_paths():
|
||||
mock_paths = MagicMock()
|
||||
mock_paths.agent_memory_file.return_value = tmp_path / "agents" / "test-agent" / "memory.json"
|
||||
@ -68,9 +71,7 @@ class TestFileMemoryStorage:
|
||||
path = storage._get_memory_file_path("test-agent")
|
||||
assert path == tmp_path / "agents" / "test-agent" / "memory.json"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_name", ["", "../etc/passwd", "agent/name", "agent\\name", "agent name", "agent@123", "agent_name"]
|
||||
)
|
||||
@pytest.mark.parametrize("invalid_name", ["", "../etc/passwd", "agent/name", "agent\\name", "agent name", "agent@123", "agent_name"])
|
||||
def test_validate_agent_name_invalid(self, invalid_name):
|
||||
"""Should raise ValueError for invalid agent names that don't match the pattern."""
|
||||
storage = FileMemoryStorage()
|
||||
@ -79,6 +80,7 @@ class TestFileMemoryStorage:
|
||||
|
||||
def test_load_creates_empty_memory(self, tmp_path):
|
||||
"""Should create empty memory when file doesn't exist."""
|
||||
|
||||
def mock_get_paths():
|
||||
mock_paths = MagicMock()
|
||||
mock_paths.memory_file = tmp_path / "non_existent_memory.json"
|
||||
@ -125,10 +127,10 @@ class TestFileMemoryStorage:
|
||||
# First load
|
||||
memory1 = storage.load()
|
||||
assert memory1["facts"][0]["content"] == "initial fact"
|
||||
|
||||
|
||||
# Update file directly
|
||||
memory_file.write_text('{"version": "1.0", "facts": [{"content": "updated fact"}]}')
|
||||
|
||||
|
||||
# Reload should get updated data
|
||||
memory2 = storage.reload()
|
||||
assert memory2["facts"][0]["content"] == "updated fact"
|
||||
@ -141,6 +143,7 @@ class TestGetMemoryStorage:
|
||||
def reset_storage_instance(self):
|
||||
"""Reset the global storage instance before and after each test."""
|
||||
import deerflow.agents.memory.storage as storage_mod
|
||||
|
||||
storage_mod._storage_instance = None
|
||||
yield
|
||||
storage_mod._storage_instance = None
|
||||
@ -167,6 +170,7 @@ class TestGetMemoryStorage:
|
||||
def test_get_memory_storage_thread_safety(self):
|
||||
"""Should safely initialize the singleton even with concurrent calls."""
|
||||
results = []
|
||||
|
||||
def get_storage():
|
||||
# get_memory_storage is called concurrently from multiple threads while
|
||||
# get_memory_config is patched once around thread creation. This verifies
|
||||
|
||||
1
frontend/.prettierignore
Normal file
1
frontend/.prettierignore
Normal file
@ -0,0 +1 @@
|
||||
pnpm-lock.yaml
|
||||
@ -10,15 +10,15 @@ DeerFlow Frontend is a Next.js 16 web interface for an AI agent system. It commu
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `pnpm dev` | Dev server with Turbopack (http://localhost:3000) |
|
||||
| `pnpm build` | Production build |
|
||||
| `pnpm check` | Lint + type check (run before committing) |
|
||||
| `pnpm lint` | ESLint only |
|
||||
| `pnpm lint:fix` | ESLint with auto-fix |
|
||||
| `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) |
|
||||
| `pnpm start` | Start production server |
|
||||
| Command | Purpose |
|
||||
| ---------------- | ------------------------------------------------- |
|
||||
| `pnpm dev` | Dev server with Turbopack (http://localhost:3000) |
|
||||
| `pnpm build` | Production build |
|
||||
| `pnpm check` | Lint + type check (run before committing) |
|
||||
| `pnpm lint` | ESLint only |
|
||||
| `pnpm lint:fix` | ESLint with auto-fix |
|
||||
| `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) |
|
||||
| `pnpm start` | Start production server |
|
||||
|
||||
No test framework is configured.
|
||||
|
||||
@ -81,6 +81,7 @@ The frontend is a stateful chat application. Users create **threads** (conversat
|
||||
## Environment
|
||||
|
||||
Backend API URLs are optional; an nginx proxy is used by default:
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_BACKEND_BASE_URL=http://localhost:8001
|
||||
NEXT_PUBLIC_LANGGRAPH_BASE_URL=http://localhost:2024
|
||||
|
||||
@ -114,17 +114,17 @@ src/
|
||||
|
||||
## Scripts
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `pnpm dev` | Start development server with Turbopack |
|
||||
| `pnpm build` | Build for production |
|
||||
| `pnpm start` | Start production server |
|
||||
| `pnpm format` | Check formatting with Prettier |
|
||||
| `pnpm format:write` | Apply formatting with Prettier |
|
||||
| `pnpm lint` | Run ESLint |
|
||||
| `pnpm lint:fix` | Fix ESLint issues |
|
||||
| `pnpm typecheck` | Run TypeScript type checking |
|
||||
| `pnpm check` | Run both lint and typecheck |
|
||||
| Command | Description |
|
||||
| ------------------- | --------------------------------------- |
|
||||
| `pnpm dev` | Start development server with Turbopack |
|
||||
| `pnpm build` | Build for production |
|
||||
| `pnpm start` | Start production server |
|
||||
| `pnpm format` | Check formatting with Prettier |
|
||||
| `pnpm format:write` | Apply formatting with Prettier |
|
||||
| `pnpm lint` | Run ESLint |
|
||||
| `pnpm lint:fix` | Fix ESLint issues |
|
||||
| `pnpm typecheck` | Run TypeScript type checking |
|
||||
| `pnpm check` | Run both lint and typecheck |
|
||||
|
||||
## Development Notes
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# Dr. Fei-Fei Li: Recent Podcast Appearances Timeline (Last 6 Months)
|
||||
|
||||
## Overview
|
||||
|
||||
Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing on major podcasts discussing the future of artificial intelligence, spatial intelligence, human-centered AI, and her work at World Labs. This timeline compiles key highlights from her recent podcast appearances from August 2025 to January 2026.
|
||||
|
||||
---
|
||||
@ -8,9 +9,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
|
||||
## Timeline of Recent Podcast Appearances
|
||||
|
||||
### January 15, 2025 - **Possible Podcast** (with Reid Hoffman and Aria Finger)
|
||||
|
||||
**Episode:** "Fei-Fei Li on spatial intelligence and human-centered AI"
|
||||
|
||||
**Key Highlights:**
|
||||
|
||||
- **Spatial Intelligence as Next Frontier:** Emphasized that spatial intelligence represents the next major evolution beyond large language models (LLMs)
|
||||
- **Human-Centered AI Philosophy:** Discussed the importance of building AI that amplifies human potential rather than replacing humans
|
||||
- **Regulatory Guardrails:** Addressed the need for thoughtful regulation and governance frameworks for AI development
|
||||
@ -22,9 +25,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
|
||||
---
|
||||
|
||||
### August 15, 2025 - **Firing Line (PBS)**
|
||||
|
||||
**Episode:** "Fei-Fei Li on ethical AI development"
|
||||
|
||||
**Key Highlights:**
|
||||
|
||||
- **Ethical AI Development:** Discussed the challenges and responsibilities in developing AI ethically
|
||||
- **Societal Impact:** Addressed how AI will transform various sectors including healthcare, education, and employment
|
||||
- **Policy Recommendations:** Provided insights on what policy frameworks are needed for responsible AI deployment
|
||||
@ -33,9 +38,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
|
||||
---
|
||||
|
||||
### November 16, 2025 - **Lenny's Podcast**
|
||||
|
||||
**Episode:** "The Godmother of AI on jobs, robots & why world models are next"
|
||||
|
||||
**Key Highlights:**
|
||||
|
||||
- **World Models Introduction:** Explained why world models and spatial intelligence represent the next frontier beyond LLMs
|
||||
- **AI Won't Replace Humans:** Argued that AI won't replace humans but will require us to take responsibility for ourselves
|
||||
- **Marble Applications:** Revealed surprising applications of World Labs' Marble product, from movie production to psychological research
|
||||
@ -44,6 +51,7 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
|
||||
- **Participation for All:** Explained how anyone can participate in AI regardless of their role or background
|
||||
|
||||
**Key Discussion Points:**
|
||||
|
||||
1. How ImageNet helped spark the current AI explosion
|
||||
2. The "bitter lesson" in AI and robotics
|
||||
3. Applications of Marble in creative industries and therapy
|
||||
@ -52,9 +60,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
|
||||
---
|
||||
|
||||
### November 25, 2025 - **Masters of Scale Summit**
|
||||
|
||||
**Episode:** "The 'Godmother of AI' on the next phase of AI" (with Reid Hoffman)
|
||||
|
||||
**Key Highlights:**
|
||||
|
||||
- **Fearless Approach:** Discussed why scientists and entrepreneurs need to be fearless in the face of an uncertain AI future
|
||||
- **Spatial Intelligence & World Modeling:** Detailed the next phase of AI focusing on spatial understanding
|
||||
- **Trust Building:** Explained how leaders should build societal trust in AI products and companies
|
||||
@ -62,6 +72,7 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
|
||||
- **Entrepreneurial Responsibility:** Argued that entrepreneurs should care about trust from day one of AI development
|
||||
|
||||
**Chapter Topics Covered:**
|
||||
|
||||
- The next phase of AI: spatial intelligence & world modeling
|
||||
- What spatial intelligence has done for humans
|
||||
- Whether AI is over-hyped
|
||||
@ -71,9 +82,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
|
||||
---
|
||||
|
||||
### December 9, 2025 - **The Tim Ferriss Show** (#839)
|
||||
|
||||
**Episode:** "Dr. Fei-Fei Li, The Godmother of AI — Asking Audacious Questions, Civilizational Technology, and Finding Your North Star"
|
||||
|
||||
**Key Highlights:**
|
||||
|
||||
- **Civilizational Technology:** Defined AI as a "civilizational technology" that will have profound economic, social, cultural, and political impacts
|
||||
- **Personal Journey:** Shared her immigrant story from Chengdu to New Jersey, and her family's seven years running a dry cleaning shop while she attended Princeton
|
||||
- **ImageNet Creation:** Detailed the creation of ImageNet and how it birthed modern AI, including innovative use of Amazon Mechanical Turk for data labeling
|
||||
@ -82,11 +95,13 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
|
||||
- **Human-Centered Focus:** Emphasized that "people are at the heart of everything" in AI development
|
||||
|
||||
**Notable Quotes:**
|
||||
|
||||
- "Really, at the end of the day, people are at the heart of everything. People made AI, people will be using AI, people will be impacted by AI, and people should have a say in AI."
|
||||
- "AI is absolutely a civilizational technology... it'll have—or [is] already having—a profound impact in the economic, social, cultural, political, downstream effects of our society."
|
||||
- "What is your North Star?"
|
||||
|
||||
**Key Topics Discussed:**
|
||||
|
||||
- From fighter jets to physics to asking "What is intelligence?"
|
||||
- The epiphany everyone missed: Big data as the hidden hypothesis
|
||||
- Against the single-genius myth: Science as non-linear lineage
|
||||
@ -97,9 +112,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
|
||||
---
|
||||
|
||||
### June 16, 2025 - **Y Combinator Startup Podcast**
|
||||
|
||||
**Episode:** "Fei-Fei Li - Spatial Intelligence is the Next Frontier in AI"
|
||||
|
||||
**Key Highlights:**
|
||||
|
||||
- **Startup Perspective:** Provided insights for AI startups on navigating the current landscape
|
||||
- **Technical Deep Dive:** Offered detailed explanations of spatial intelligence technologies
|
||||
- **Entrepreneurial Advice:** Shared lessons from transitioning from academia to entrepreneurship
|
||||
@ -110,26 +127,31 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
|
||||
## Common Themes Across Recent Appearances
|
||||
|
||||
### 1. **Spatial Intelligence as the Next Frontier**
|
||||
|
||||
- Repeated emphasis that spatial intelligence represents the next major evolution beyond language models
|
||||
- World Labs' focus on creating AI that understands and interacts with the physical world
|
||||
- Applications ranging from robotics and autonomous systems to creative industries and therapy
|
||||
|
||||
### 2. **Human-Centered AI Philosophy**
|
||||
|
||||
- Consistent message that AI should augment rather than replace human capabilities
|
||||
- Emphasis on maintaining human agency and responsibility in AI systems
|
||||
- Focus on building trust and ethical frameworks
|
||||
|
||||
### 3. **Educational Transformation**
|
||||
|
||||
- Advocacy for integrating AI into education to enhance learning
|
||||
- Proposal to use AI as a benchmark for student improvement
|
||||
- Emphasis on making AI accessible to people from all backgrounds
|
||||
|
||||
### 4. **Historical Perspective**
|
||||
|
||||
- Frequent references to ImageNet's role in sparking the deep learning revolution
|
||||
- Context about how rapidly the AI landscape has changed
|
||||
- Emphasis on collaborative, non-linear progress in scientific advancement
|
||||
|
||||
### 5. **Entrepreneurial Vision**
|
||||
|
||||
- Insights on building AI companies in the current environment
|
||||
- Balance between technological innovation and responsible development
|
||||
- Focus on practical applications that solve real-world problems
|
||||
@ -139,18 +161,21 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
|
||||
## Key Insights and Predictions
|
||||
|
||||
### **Near-Term Developments (1-3 years):**
|
||||
|
||||
- Rapid advancement in spatial intelligence and world modeling technologies
|
||||
- Increased integration of AI in education and creative industries
|
||||
- Growing focus on AI ethics and governance frameworks
|
||||
- Expansion of practical applications in healthcare, therapy, and accessibility
|
||||
|
||||
### **Medium-Term Vision (3-5 years):**
|
||||
|
||||
- More sophisticated human-AI collaboration systems
|
||||
- Breakthroughs in robotics enabled by spatial intelligence
|
||||
- Transformation of how we teach and learn with AI assistance
|
||||
- Development of new industries centered around spatial AI
|
||||
|
||||
### **Long-Term Philosophy:**
|
||||
|
||||
- AI as a "civilizational technology" that requires thoughtful stewardship
|
||||
- Emphasis on maintaining human values and agency in technological progress
|
||||
- Vision of technology that helps humanity "raise above our paleolithic emotions"
|
||||
@ -166,6 +191,7 @@ The timeline shows her evolving role from academic researcher to entrepreneur wh
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
1. The Tim Ferriss Show (December 9, 2025)
|
||||
2. Lenny's Podcast (November 16, 2025)
|
||||
3. Masters of Scale Summit (November 25, 2025)
|
||||
@ -173,4 +199,4 @@ The timeline shows her evolving role from academic researcher to entrepreneur wh
|
||||
5. Firing Line, PBS (August 15, 2025)
|
||||
6. Y Combinator Startup Podcast (June 16, 2025)
|
||||
|
||||
*Compiled on January 25, 2026*
|
||||
_Compiled on January 25, 2026_
|
||||
|
||||
@ -802,4 +802,4 @@
|
||||
"interrupts": [],
|
||||
"checkpoint_id": "1f0f46d7-77ea-64ca-802c-0462f9bf4fdd",
|
||||
"parent_checkpoint_id": "1f0f46d7-77e2-6496-802b-68a165ed83e9"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1 +1,4 @@
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚽</text></svg>">
|
||||
<link
|
||||
rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚽</text></svg>"
|
||||
/>
|
||||
|
||||
@ -1,365 +1,385 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>江苏城市足球联赛2025赛季 | 苏超联赛第一季</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&family=Oswald:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚽</text></svg>">
|
||||
</head>
|
||||
<body>
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&family=Oswald:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚽</text></svg>"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 加载动画 -->
|
||||
<div class="loader">
|
||||
<div class="loader-content">
|
||||
<div class="football"></div>
|
||||
<div class="loader-text">加载中...</div>
|
||||
</div>
|
||||
<div class="loader-content">
|
||||
<div class="football"></div>
|
||||
<div class="loader-text">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<div class="nav-brand">
|
||||
<div class="logo">
|
||||
<div class="logo-ball"></div>
|
||||
<span class="logo-text">苏超联赛</span>
|
||||
</div>
|
||||
<div class="league-name">江苏城市足球联赛2025赛季</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu">
|
||||
<a href="#home" class="nav-link active">首页</a>
|
||||
<a href="#teams" class="nav-link">球队</a>
|
||||
<a href="#fixtures" class="nav-link">赛程</a>
|
||||
<a href="#standings" class="nav-link">积分榜</a>
|
||||
<a href="#stats" class="nav-link">数据</a>
|
||||
<a href="#news" class="nav-link">新闻</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-actions">
|
||||
<button class="btn-theme-toggle">
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
<button class="btn-menu-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="nav-brand">
|
||||
<div class="logo">
|
||||
<div class="logo-ball"></div>
|
||||
<span class="logo-text">苏超联赛</span>
|
||||
</div>
|
||||
<div class="league-name">江苏城市足球联赛2025赛季</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu">
|
||||
<a href="#home" class="nav-link active">首页</a>
|
||||
<a href="#teams" class="nav-link">球队</a>
|
||||
<a href="#fixtures" class="nav-link">赛程</a>
|
||||
<a href="#standings" class="nav-link">积分榜</a>
|
||||
<a href="#stats" class="nav-link">数据</a>
|
||||
<a href="#news" class="nav-link">新闻</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-actions">
|
||||
<button class="btn-theme-toggle">
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
<button class="btn-menu-toggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main>
|
||||
<!-- 英雄区域 -->
|
||||
<section id="home" class="hero">
|
||||
<div class="hero-background">
|
||||
<div class="hero-gradient"></div>
|
||||
<div class="hero-pattern"></div>
|
||||
<div class="hero-ball-animation"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<div class="hero-badge">
|
||||
<span class="badge-season">2025赛季</span>
|
||||
<span class="badge-league">苏超联赛第一季</span>
|
||||
</div>
|
||||
|
||||
<h1 class="hero-title">
|
||||
<span class="title-line">江苏城市</span>
|
||||
<span class="title-line highlight">足球联赛</span>
|
||||
</h1>
|
||||
|
||||
<p class="hero-subtitle">
|
||||
江苏省首个城市间职业足球联赛,汇集12支精英球队,点燃2025赛季战火!
|
||||
</p>
|
||||
|
||||
<div class="hero-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">12</div>
|
||||
<div class="stat-label">参赛球队</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">132</div>
|
||||
<div class="stat-label">场比赛</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">26</div>
|
||||
<div class="stat-label">比赛周</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">1</div>
|
||||
<div class="stat-label">冠军荣耀</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-actions">
|
||||
<a href="#fixtures" class="btn btn-primary">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
查看赛程
|
||||
</a>
|
||||
<a href="#standings" class="btn btn-secondary">
|
||||
<i class="fas fa-trophy"></i>
|
||||
积分榜
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-visual">
|
||||
<div class="stadium-visual">
|
||||
<div class="stadium-field"></div>
|
||||
<div class="stadium-stands"></div>
|
||||
<div class="stadium-players">
|
||||
<div class="player player-1"></div>
|
||||
<div class="player player-2"></div>
|
||||
<div class="player player-3"></div>
|
||||
</div>
|
||||
<div class="stadium-ball"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-scroll">
|
||||
<div class="scroll-indicator">
|
||||
<div class="scroll-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 英雄区域 -->
|
||||
<section id="home" class="hero">
|
||||
<div class="hero-background">
|
||||
<div class="hero-gradient"></div>
|
||||
<div class="hero-pattern"></div>
|
||||
<div class="hero-ball-animation"></div>
|
||||
</div>
|
||||
|
||||
<!-- 下一场比赛 -->
|
||||
<section class="next-match">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">下一场比赛</h2>
|
||||
<div class="section-subtitle">即将开始的精彩对决</div>
|
||||
</div>
|
||||
|
||||
<div class="match-card">
|
||||
<div class="match-date">
|
||||
<div class="match-day">周六</div>
|
||||
<div class="match-date-number">25</div>
|
||||
<div class="match-month">一月</div>
|
||||
<div class="match-time">19:30</div>
|
||||
</div>
|
||||
|
||||
<div class="match-teams">
|
||||
<div class="team team-home">
|
||||
<div class="team-logo logo-nanjing"></div>
|
||||
<div class="team-name">南京城联</div>
|
||||
<div class="team-record">8胜 3平 2负</div>
|
||||
</div>
|
||||
|
||||
<div class="match-vs">
|
||||
<div class="vs-text">VS</div>
|
||||
<div class="match-info">
|
||||
<div class="match-venue">南京奥体中心</div>
|
||||
<div class="match-round">第12轮</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="team team-away">
|
||||
<div class="team-logo logo-suzhou"></div>
|
||||
<div class="team-name">苏州雄狮</div>
|
||||
<div class="team-record">7胜 4平 2负</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="match-actions">
|
||||
<button class="btn btn-outline">
|
||||
<i class="fas fa-bell"></i>
|
||||
设置提醒
|
||||
</button>
|
||||
<button class="btn btn-primary">
|
||||
<i class="fas fa-ticket-alt"></i>
|
||||
购票
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<div class="hero-badge">
|
||||
<span class="badge-season">2025赛季</span>
|
||||
<span class="badge-league">苏超联赛第一季</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 球队展示 -->
|
||||
<section id="teams" class="teams-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">参赛球队</h2>
|
||||
<div class="section-subtitle">12支城市代表队的荣耀之战</div>
|
||||
</div>
|
||||
|
||||
<div class="teams-grid">
|
||||
<!-- 球队卡片将通过JS动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<h1 class="hero-title">
|
||||
<span class="title-line">江苏城市</span>
|
||||
<span class="title-line highlight">足球联赛</span>
|
||||
</h1>
|
||||
|
||||
<!-- 积分榜 -->
|
||||
<section id="standings" class="standings-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">积分榜</h2>
|
||||
<div class="section-subtitle">2025赛季实时排名</div>
|
||||
</div>
|
||||
|
||||
<div class="standings-container">
|
||||
<div class="standings-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>排名</th>
|
||||
<th>球队</th>
|
||||
<th>场次</th>
|
||||
<th>胜</th>
|
||||
<th>平</th>
|
||||
<th>负</th>
|
||||
<th>进球</th>
|
||||
<th>失球</th>
|
||||
<th>净胜球</th>
|
||||
<th>积分</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- 积分榜数据将通过JS动态生成 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<p class="hero-subtitle">
|
||||
江苏省首个城市间职业足球联赛,汇集12支精英球队,点燃2025赛季战火!
|
||||
</p>
|
||||
|
||||
<!-- 赛程表 -->
|
||||
<section id="fixtures" class="fixtures-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">赛程表</h2>
|
||||
<div class="section-subtitle">2025赛季完整赛程</div>
|
||||
</div>
|
||||
|
||||
<div class="fixtures-tabs">
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-round="all">全部赛程</button>
|
||||
<button class="tab" data-round="next">即将比赛</button>
|
||||
<button class="tab" data-round="recent">最近赛果</button>
|
||||
</div>
|
||||
|
||||
<div class="fixtures-list">
|
||||
<!-- 赛程数据将通过JS动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">12</div>
|
||||
<div class="stat-label">参赛球队</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">132</div>
|
||||
<div class="stat-label">场比赛</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">26</div>
|
||||
<div class="stat-label">比赛周</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">1</div>
|
||||
<div class="stat-label">冠军荣耀</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<section id="stats" class="stats-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">数据统计</h2>
|
||||
<div class="section-subtitle">球员与球队数据排行榜</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-tabs">
|
||||
<div class="stats-tab-nav">
|
||||
<button class="stats-tab active" data-tab="scorers">射手榜</button>
|
||||
<button class="stats-tab" data-tab="assists">助攻榜</button>
|
||||
<button class="stats-tab" data-tab="teams">球队数据</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-content">
|
||||
<div class="stats-tab-content active" id="scorers">
|
||||
<!-- 射手榜数据 -->
|
||||
</div>
|
||||
<div class="stats-tab-content" id="assists">
|
||||
<!-- 助攻榜数据 -->
|
||||
</div>
|
||||
<div class="stats-tab-content" id="teams">
|
||||
<!-- 球队数据 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a href="#fixtures" class="btn btn-primary">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
查看赛程
|
||||
</a>
|
||||
<a href="#standings" class="btn btn-secondary">
|
||||
<i class="fas fa-trophy"></i>
|
||||
积分榜
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 新闻动态 -->
|
||||
<section id="news" class="news-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">新闻动态</h2>
|
||||
<div class="section-subtitle">联赛最新资讯</div>
|
||||
</div>
|
||||
|
||||
<div class="news-grid">
|
||||
<!-- 新闻卡片将通过JS动态生成 -->
|
||||
</div>
|
||||
<div class="hero-visual">
|
||||
<div class="stadium-visual">
|
||||
<div class="stadium-field"></div>
|
||||
<div class="stadium-stands"></div>
|
||||
<div class="stadium-players">
|
||||
<div class="player player-1"></div>
|
||||
<div class="player player-2"></div>
|
||||
<div class="player player-3"></div>
|
||||
</div>
|
||||
<div class="stadium-ball"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<div class="logo">
|
||||
<div class="logo-ball"></div>
|
||||
<span class="logo-text">苏超联赛</span>
|
||||
</div>
|
||||
<div class="footer-description">
|
||||
江苏城市足球联赛2025赛季官方网站
|
||||
</div>
|
||||
<div class="footer-social">
|
||||
<a href="#" class="social-link"><i class="fab fa-weibo"></i></a>
|
||||
<a href="#" class="social-link"><i class="fab fa-weixin"></i></a>
|
||||
<a href="#" class="social-link"><i class="fab fa-douyin"></i></a>
|
||||
<a href="#" class="social-link"><i class="fab fa-bilibili"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<div class="footer-column">
|
||||
<h3 class="footer-title">联赛信息</h3>
|
||||
<a href="#" class="footer-link">关于联赛</a>
|
||||
<a href="#" class="footer-link">联赛章程</a>
|
||||
<a href="#" class="footer-link">组织机构</a>
|
||||
<a href="#" class="footer-link">合作伙伴</a>
|
||||
</div>
|
||||
|
||||
<div class="footer-column">
|
||||
<h3 class="footer-title">球迷服务</h3>
|
||||
<a href="#" class="footer-link">票务信息</a>
|
||||
<a href="#" class="footer-link">球迷社区</a>
|
||||
<a href="#" class="footer-link">官方商店</a>
|
||||
<a href="#" class="footer-link">联系我们</a>
|
||||
</div>
|
||||
|
||||
<div class="footer-column">
|
||||
<h3 class="footer-title">媒体中心</h3>
|
||||
<a href="#" class="footer-link">新闻发布</a>
|
||||
<a href="#" class="footer-link">媒体资料</a>
|
||||
<a href="#" class="footer-link">采访申请</a>
|
||||
<a href="#" class="footer-link">摄影图库</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<div class="copyright">
|
||||
© 2025 江苏城市足球联赛. 保留所有权利.
|
||||
</div>
|
||||
<div class="footer-legal">
|
||||
<a href="#" class="legal-link">隐私政策</a>
|
||||
<a href="#" class="legal-link">使用条款</a>
|
||||
<a href="#" class="legal-link">Cookie政策</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-scroll">
|
||||
<div class="scroll-indicator">
|
||||
<div class="scroll-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 下一场比赛 -->
|
||||
<section class="next-match">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">下一场比赛</h2>
|
||||
<div class="section-subtitle">即将开始的精彩对决</div>
|
||||
</div>
|
||||
|
||||
<div class="match-card">
|
||||
<div class="match-date">
|
||||
<div class="match-day">周六</div>
|
||||
<div class="match-date-number">25</div>
|
||||
<div class="match-month">一月</div>
|
||||
<div class="match-time">19:30</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="match-teams">
|
||||
<div class="team team-home">
|
||||
<div class="team-logo logo-nanjing"></div>
|
||||
<div class="team-name">南京城联</div>
|
||||
<div class="team-record">8胜 3平 2负</div>
|
||||
</div>
|
||||
|
||||
<div class="match-vs">
|
||||
<div class="vs-text">VS</div>
|
||||
<div class="match-info">
|
||||
<div class="match-venue">南京奥体中心</div>
|
||||
<div class="match-round">第12轮</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="team team-away">
|
||||
<div class="team-logo logo-suzhou"></div>
|
||||
<div class="team-name">苏州雄狮</div>
|
||||
<div class="team-record">7胜 4平 2负</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="match-actions">
|
||||
<button class="btn btn-outline">
|
||||
<i class="fas fa-bell"></i>
|
||||
设置提醒
|
||||
</button>
|
||||
<button class="btn btn-primary">
|
||||
<i class="fas fa-ticket-alt"></i>
|
||||
购票
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 球队展示 -->
|
||||
<section id="teams" class="teams-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">参赛球队</h2>
|
||||
<div class="section-subtitle">12支城市代表队的荣耀之战</div>
|
||||
</div>
|
||||
|
||||
<div class="teams-grid">
|
||||
<!-- 球队卡片将通过JS动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 积分榜 -->
|
||||
<section id="standings" class="standings-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">积分榜</h2>
|
||||
<div class="section-subtitle">2025赛季实时排名</div>
|
||||
</div>
|
||||
|
||||
<div class="standings-container">
|
||||
<div class="standings-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>排名</th>
|
||||
<th>球队</th>
|
||||
<th>场次</th>
|
||||
<th>胜</th>
|
||||
<th>平</th>
|
||||
<th>负</th>
|
||||
<th>进球</th>
|
||||
<th>失球</th>
|
||||
<th>净胜球</th>
|
||||
<th>积分</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- 积分榜数据将通过JS动态生成 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 赛程表 -->
|
||||
<section id="fixtures" class="fixtures-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">赛程表</h2>
|
||||
<div class="section-subtitle">2025赛季完整赛程</div>
|
||||
</div>
|
||||
|
||||
<div class="fixtures-tabs">
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-round="all">全部赛程</button>
|
||||
<button class="tab" data-round="next">即将比赛</button>
|
||||
<button class="tab" data-round="recent">最近赛果</button>
|
||||
</div>
|
||||
|
||||
<div class="fixtures-list">
|
||||
<!-- 赛程数据将通过JS动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<section id="stats" class="stats-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">数据统计</h2>
|
||||
<div class="section-subtitle">球员与球队数据排行榜</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-tabs">
|
||||
<div class="stats-tab-nav">
|
||||
<button class="stats-tab active" data-tab="scorers">
|
||||
射手榜
|
||||
</button>
|
||||
<button class="stats-tab" data-tab="assists">助攻榜</button>
|
||||
<button class="stats-tab" data-tab="teams">球队数据</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-content">
|
||||
<div class="stats-tab-content active" id="scorers">
|
||||
<!-- 射手榜数据 -->
|
||||
</div>
|
||||
<div class="stats-tab-content" id="assists">
|
||||
<!-- 助攻榜数据 -->
|
||||
</div>
|
||||
<div class="stats-tab-content" id="teams">
|
||||
<!-- 球队数据 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 新闻动态 -->
|
||||
<section id="news" class="news-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">新闻动态</h2>
|
||||
<div class="section-subtitle">联赛最新资讯</div>
|
||||
</div>
|
||||
|
||||
<div class="news-grid">
|
||||
<!-- 新闻卡片将通过JS动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 底部 -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<div class="logo">
|
||||
<div class="logo-ball"></div>
|
||||
<span class="logo-text">苏超联赛</span>
|
||||
</div>
|
||||
<div class="footer-description">
|
||||
江苏城市足球联赛2025赛季官方网站
|
||||
</div>
|
||||
<div class="footer-social">
|
||||
<a href="#" class="social-link"><i class="fab fa-weibo"></i></a>
|
||||
<a href="#" class="social-link"
|
||||
><i class="fab fa-weixin"></i
|
||||
></a>
|
||||
<a href="#" class="social-link"
|
||||
><i class="fab fa-douyin"></i
|
||||
></a>
|
||||
<a href="#" class="social-link"
|
||||
><i class="fab fa-bilibili"></i
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<div class="footer-column">
|
||||
<h3 class="footer-title">联赛信息</h3>
|
||||
<a href="#" class="footer-link">关于联赛</a>
|
||||
<a href="#" class="footer-link">联赛章程</a>
|
||||
<a href="#" class="footer-link">组织机构</a>
|
||||
<a href="#" class="footer-link">合作伙伴</a>
|
||||
</div>
|
||||
|
||||
<div class="footer-column">
|
||||
<h3 class="footer-title">球迷服务</h3>
|
||||
<a href="#" class="footer-link">票务信息</a>
|
||||
<a href="#" class="footer-link">球迷社区</a>
|
||||
<a href="#" class="footer-link">官方商店</a>
|
||||
<a href="#" class="footer-link">联系我们</a>
|
||||
</div>
|
||||
|
||||
<div class="footer-column">
|
||||
<h3 class="footer-title">媒体中心</h3>
|
||||
<a href="#" class="footer-link">新闻发布</a>
|
||||
<a href="#" class="footer-link">媒体资料</a>
|
||||
<a href="#" class="footer-link">采访申请</a>
|
||||
<a href="#" class="footer-link">摄影图库</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<div class="copyright">
|
||||
© 2025 江苏城市足球联赛. 保留所有权利.
|
||||
</div>
|
||||
<div class="footer-legal">
|
||||
<a href="#" class="legal-link">隐私政策</a>
|
||||
<a href="#" class="legal-link">使用条款</a>
|
||||
<a href="#" class="legal-link">Cookie政策</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<!-- JavaScript文件 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
|
||||
<script src="js/data.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,163 +1,163 @@
|
||||
// 江苏城市足球联赛2025赛季 - 主JavaScript文件
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化加载动画
|
||||
initLoader();
|
||||
|
||||
// 初始化主题切换
|
||||
initThemeToggle();
|
||||
|
||||
// 初始化导航菜单
|
||||
initNavigation();
|
||||
|
||||
// 初始化滚动监听
|
||||
initScrollSpy();
|
||||
|
||||
// 渲染球队卡片
|
||||
renderTeams();
|
||||
|
||||
// 渲染积分榜
|
||||
renderStandings();
|
||||
|
||||
// 渲染赛程表
|
||||
renderFixtures();
|
||||
|
||||
// 渲染数据统计
|
||||
renderStats();
|
||||
|
||||
// 渲染新闻动态
|
||||
renderNews();
|
||||
|
||||
// 初始化标签页切换
|
||||
initTabs();
|
||||
|
||||
// 初始化移动端菜单
|
||||
initMobileMenu();
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// 初始化加载动画
|
||||
initLoader();
|
||||
|
||||
// 初始化主题切换
|
||||
initThemeToggle();
|
||||
|
||||
// 初始化导航菜单
|
||||
initNavigation();
|
||||
|
||||
// 初始化滚动监听
|
||||
initScrollSpy();
|
||||
|
||||
// 渲染球队卡片
|
||||
renderTeams();
|
||||
|
||||
// 渲染积分榜
|
||||
renderStandings();
|
||||
|
||||
// 渲染赛程表
|
||||
renderFixtures();
|
||||
|
||||
// 渲染数据统计
|
||||
renderStats();
|
||||
|
||||
// 渲染新闻动态
|
||||
renderNews();
|
||||
|
||||
// 初始化标签页切换
|
||||
initTabs();
|
||||
|
||||
// 初始化移动端菜单
|
||||
initMobileMenu();
|
||||
});
|
||||
|
||||
// 加载动画
|
||||
function initLoader() {
|
||||
const loader = document.querySelector('.loader');
|
||||
|
||||
// 模拟加载延迟
|
||||
const loader = document.querySelector(".loader");
|
||||
|
||||
// 模拟加载延迟
|
||||
setTimeout(() => {
|
||||
loader.classList.add("loaded");
|
||||
|
||||
// 动画结束后隐藏loader
|
||||
setTimeout(() => {
|
||||
loader.classList.add('loaded');
|
||||
|
||||
// 动画结束后隐藏loader
|
||||
setTimeout(() => {
|
||||
loader.style.display = 'none';
|
||||
}, 300);
|
||||
}, 1500);
|
||||
loader.style.display = "none";
|
||||
}, 300);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// 主题切换
|
||||
function initThemeToggle() {
|
||||
const themeToggle = document.querySelector('.btn-theme-toggle');
|
||||
const themeIcon = themeToggle.querySelector('i');
|
||||
|
||||
// 检查本地存储的主题偏好
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
updateThemeIcon(savedTheme);
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
updateThemeIcon(newTheme);
|
||||
|
||||
// 添加切换动画
|
||||
themeToggle.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => {
|
||||
themeToggle.style.transform = '';
|
||||
}, 150);
|
||||
});
|
||||
|
||||
function updateThemeIcon(theme) {
|
||||
if (theme === 'dark') {
|
||||
themeIcon.className = 'fas fa-sun';
|
||||
} else {
|
||||
themeIcon.className = 'fas fa-moon';
|
||||
}
|
||||
const themeToggle = document.querySelector(".btn-theme-toggle");
|
||||
const themeIcon = themeToggle.querySelector("i");
|
||||
|
||||
// 检查本地存储的主题偏好
|
||||
const savedTheme = localStorage.getItem("theme") || "light";
|
||||
document.documentElement.setAttribute("data-theme", savedTheme);
|
||||
updateThemeIcon(savedTheme);
|
||||
|
||||
themeToggle.addEventListener("click", () => {
|
||||
const currentTheme = document.documentElement.getAttribute("data-theme");
|
||||
const newTheme = currentTheme === "light" ? "dark" : "light";
|
||||
|
||||
document.documentElement.setAttribute("data-theme", newTheme);
|
||||
localStorage.setItem("theme", newTheme);
|
||||
updateThemeIcon(newTheme);
|
||||
|
||||
// 添加切换动画
|
||||
themeToggle.style.transform = "scale(0.9)";
|
||||
setTimeout(() => {
|
||||
themeToggle.style.transform = "";
|
||||
}, 150);
|
||||
});
|
||||
|
||||
function updateThemeIcon(theme) {
|
||||
if (theme === "dark") {
|
||||
themeIcon.className = "fas fa-sun";
|
||||
} else {
|
||||
themeIcon.className = "fas fa-moon";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导航菜单
|
||||
function initNavigation() {
|
||||
const navLinks = document.querySelectorAll('.nav-link');
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetSection = document.querySelector(targetId);
|
||||
|
||||
if (targetSection) {
|
||||
// 更新活动链接
|
||||
navLinks.forEach(l => l.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// 平滑滚动到目标区域
|
||||
window.scrollTo({
|
||||
top: targetSection.offsetTop - 80,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// 如果是移动端,关闭菜单
|
||||
const navMenu = document.querySelector('.nav-menu');
|
||||
if (navMenu.classList.contains('active')) {
|
||||
navMenu.classList.remove('active');
|
||||
}
|
||||
}
|
||||
const navLinks = document.querySelectorAll(".nav-link");
|
||||
|
||||
navLinks.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = this.getAttribute("href");
|
||||
const targetSection = document.querySelector(targetId);
|
||||
|
||||
if (targetSection) {
|
||||
// 更新活动链接
|
||||
navLinks.forEach((l) => l.classList.remove("active"));
|
||||
this.classList.add("active");
|
||||
|
||||
// 平滑滚动到目标区域
|
||||
window.scrollTo({
|
||||
top: targetSection.offsetTop - 80,
|
||||
behavior: "smooth",
|
||||
});
|
||||
|
||||
// 如果是移动端,关闭菜单
|
||||
const navMenu = document.querySelector(".nav-menu");
|
||||
if (navMenu.classList.contains("active")) {
|
||||
navMenu.classList.remove("active");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 滚动监听
|
||||
function initScrollSpy() {
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
const navLinks = document.querySelectorAll('.nav-link');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
|
||||
if (scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === `#${current}`) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
const sections = document.querySelectorAll("section[id]");
|
||||
const navLinks = document.querySelectorAll(".nav-link");
|
||||
|
||||
window.addEventListener("scroll", () => {
|
||||
let current = "";
|
||||
|
||||
sections.forEach((section) => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
|
||||
if (scrollY >= sectionTop - 100) {
|
||||
current = section.getAttribute("id");
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach((link) => {
|
||||
link.classList.remove("active");
|
||||
if (link.getAttribute("href") === `#${current}`) {
|
||||
link.classList.add("active");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染球队卡片
|
||||
function renderTeams() {
|
||||
const teamsGrid = document.querySelector('.teams-grid');
|
||||
|
||||
if (!teamsGrid) return;
|
||||
|
||||
teamsGrid.innerHTML = '';
|
||||
|
||||
leagueData.teams.forEach(team => {
|
||||
const teamCard = document.createElement('div');
|
||||
teamCard.className = 'team-card';
|
||||
|
||||
// 获取球队统计数据
|
||||
const standing = leagueData.standings.find(s => s.teamId === team.id);
|
||||
|
||||
teamCard.innerHTML = `
|
||||
const teamsGrid = document.querySelector(".teams-grid");
|
||||
|
||||
if (!teamsGrid) return;
|
||||
|
||||
teamsGrid.innerHTML = "";
|
||||
|
||||
leagueData.teams.forEach((team) => {
|
||||
const teamCard = document.createElement("div");
|
||||
teamCard.className = "team-card";
|
||||
|
||||
// 获取球队统计数据
|
||||
const standing = leagueData.standings.find((s) => s.teamId === team.id);
|
||||
|
||||
teamCard.innerHTML = `
|
||||
<div class="team-card-logo" style="background: linear-gradient(135deg, ${team.colors[0]} 0%, ${team.colors[1]} 100%);">
|
||||
${team.shortName}
|
||||
</div>
|
||||
@ -165,52 +165,52 @@ function renderTeams() {
|
||||
<div class="team-card-city">${team.city}</div>
|
||||
<div class="team-card-stats">
|
||||
<div class="team-stat">
|
||||
<div class="team-stat-value">${standing ? standing.rank : '-'}</div>
|
||||
<div class="team-stat-value">${standing ? standing.rank : "-"}</div>
|
||||
<div class="team-stat-label">排名</div>
|
||||
</div>
|
||||
<div class="team-stat">
|
||||
<div class="team-stat-value">${standing ? standing.points : '0'}</div>
|
||||
<div class="team-stat-value">${standing ? standing.points : "0"}</div>
|
||||
<div class="team-stat-label">积分</div>
|
||||
</div>
|
||||
<div class="team-stat">
|
||||
<div class="team-stat-value">${standing ? standing.goalDifference : '0'}</div>
|
||||
<div class="team-stat-value">${standing ? standing.goalDifference : "0"}</div>
|
||||
<div class="team-stat-label">净胜球</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
teamCard.addEventListener('click', () => {
|
||||
// 这里可以添加点击跳转到球队详情页的功能
|
||||
alert(`查看 ${team.name} 的详细信息`);
|
||||
});
|
||||
|
||||
teamsGrid.appendChild(teamCard);
|
||||
|
||||
teamCard.addEventListener("click", () => {
|
||||
// 这里可以添加点击跳转到球队详情页的功能
|
||||
alert(`查看 ${team.name} 的详细信息`);
|
||||
});
|
||||
|
||||
teamsGrid.appendChild(teamCard);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染积分榜
|
||||
function renderStandings() {
|
||||
const standingsTable = document.querySelector('.standings-table tbody');
|
||||
|
||||
if (!standingsTable) return;
|
||||
|
||||
standingsTable.innerHTML = '';
|
||||
|
||||
leagueData.standings.forEach(standing => {
|
||||
const team = getTeamById(standing.teamId);
|
||||
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// 根据排名添加特殊样式
|
||||
if (standing.rank <= 4) {
|
||||
row.classList.add('champions-league');
|
||||
} else if (standing.rank <= 6) {
|
||||
row.classList.add('europa-league');
|
||||
} else if (standing.rank >= 11) {
|
||||
row.classList.add('relegation');
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
const standingsTable = document.querySelector(".standings-table tbody");
|
||||
|
||||
if (!standingsTable) return;
|
||||
|
||||
standingsTable.innerHTML = "";
|
||||
|
||||
leagueData.standings.forEach((standing) => {
|
||||
const team = getTeamById(standing.teamId);
|
||||
|
||||
const row = document.createElement("tr");
|
||||
|
||||
// 根据排名添加特殊样式
|
||||
if (standing.rank <= 4) {
|
||||
row.classList.add("champions-league");
|
||||
} else if (standing.rank <= 6) {
|
||||
row.classList.add("europa-league");
|
||||
} else if (standing.rank >= 11) {
|
||||
row.classList.add("relegation");
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${standing.rank}</td>
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
@ -224,70 +224,80 @@ function renderStandings() {
|
||||
<td>${standing.lost}</td>
|
||||
<td>${standing.goalsFor}</td>
|
||||
<td>${standing.goalsAgainst}</td>
|
||||
<td>${standing.goalDifference > 0 ? '+' : ''}${standing.goalDifference}</td>
|
||||
<td>${standing.goalDifference > 0 ? "+" : ""}${standing.goalDifference}</td>
|
||||
<td><strong>${standing.points}</strong></td>
|
||||
`;
|
||||
|
||||
standingsTable.appendChild(row);
|
||||
});
|
||||
|
||||
standingsTable.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染赛程表
|
||||
function renderFixtures() {
|
||||
const fixturesList = document.querySelector('.fixtures-list');
|
||||
|
||||
if (!fixturesList) return;
|
||||
|
||||
fixturesList.innerHTML = '';
|
||||
|
||||
// 按轮次分组
|
||||
const fixturesByRound = {};
|
||||
leagueData.fixtures.forEach(fixture => {
|
||||
if (!fixturesByRound[fixture.round]) {
|
||||
fixturesByRound[fixture.round] = [];
|
||||
}
|
||||
fixturesByRound[fixture.round].push(fixture);
|
||||
});
|
||||
|
||||
// 渲染所有赛程
|
||||
Object.keys(fixturesByRound).sort((a, b) => a - b).forEach(round => {
|
||||
const roundHeader = document.createElement('div');
|
||||
roundHeader.className = 'fixture-round-header';
|
||||
roundHeader.innerHTML = `<h3>第${round}轮</h3>`;
|
||||
fixturesList.appendChild(roundHeader);
|
||||
|
||||
fixturesByRound[round].forEach(fixture => {
|
||||
const homeTeam = getTeamById(fixture.homeTeamId);
|
||||
const awayTeam = getTeamById(fixture.awayTeamId);
|
||||
|
||||
const fixtureItem = document.createElement('div');
|
||||
fixtureItem.className = 'fixture-item';
|
||||
|
||||
const date = new Date(fixture.date);
|
||||
const dayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
const dayName = dayNames[date.getDay()];
|
||||
|
||||
let scoreHtml = '';
|
||||
let statusText = '';
|
||||
|
||||
if (fixture.status === 'completed') {
|
||||
scoreHtml = `
|
||||
const fixturesList = document.querySelector(".fixtures-list");
|
||||
|
||||
if (!fixturesList) return;
|
||||
|
||||
fixturesList.innerHTML = "";
|
||||
|
||||
// 按轮次分组
|
||||
const fixturesByRound = {};
|
||||
leagueData.fixtures.forEach((fixture) => {
|
||||
if (!fixturesByRound[fixture.round]) {
|
||||
fixturesByRound[fixture.round] = [];
|
||||
}
|
||||
fixturesByRound[fixture.round].push(fixture);
|
||||
});
|
||||
|
||||
// 渲染所有赛程
|
||||
Object.keys(fixturesByRound)
|
||||
.sort((a, b) => a - b)
|
||||
.forEach((round) => {
|
||||
const roundHeader = document.createElement("div");
|
||||
roundHeader.className = "fixture-round-header";
|
||||
roundHeader.innerHTML = `<h3>第${round}轮</h3>`;
|
||||
fixturesList.appendChild(roundHeader);
|
||||
|
||||
fixturesByRound[round].forEach((fixture) => {
|
||||
const homeTeam = getTeamById(fixture.homeTeamId);
|
||||
const awayTeam = getTeamById(fixture.awayTeamId);
|
||||
|
||||
const fixtureItem = document.createElement("div");
|
||||
fixtureItem.className = "fixture-item";
|
||||
|
||||
const date = new Date(fixture.date);
|
||||
const dayNames = [
|
||||
"周日",
|
||||
"周一",
|
||||
"周二",
|
||||
"周三",
|
||||
"周四",
|
||||
"周五",
|
||||
"周六",
|
||||
];
|
||||
const dayName = dayNames[date.getDay()];
|
||||
|
||||
let scoreHtml = "";
|
||||
let statusText = "";
|
||||
|
||||
if (fixture.status === "completed") {
|
||||
scoreHtml = `
|
||||
<div class="fixture-score-value">${fixture.homeScore} - ${fixture.awayScore}</div>
|
||||
<div class="fixture-score-status">已结束</div>
|
||||
`;
|
||||
} else if (fixture.status === 'scheduled') {
|
||||
scoreHtml = `
|
||||
} else if (fixture.status === "scheduled") {
|
||||
scoreHtml = `
|
||||
<div class="fixture-score-value">VS</div>
|
||||
<div class="fixture-score-status">${fixture.time}</div>
|
||||
`;
|
||||
} else {
|
||||
scoreHtml = `
|
||||
} else {
|
||||
scoreHtml = `
|
||||
<div class="fixture-score-value">-</div>
|
||||
<div class="fixture-score-status">待定</div>
|
||||
`;
|
||||
}
|
||||
|
||||
fixtureItem.innerHTML = `
|
||||
}
|
||||
|
||||
fixtureItem.innerHTML = `
|
||||
<div class="fixture-date">
|
||||
<div class="fixture-day">${dayName}</div>
|
||||
<div class="fixture-time">${formatDate(fixture.date)}</div>
|
||||
@ -307,25 +317,25 @@ function renderFixtures() {
|
||||
${scoreHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
fixturesList.appendChild(fixtureItem);
|
||||
});
|
||||
|
||||
fixturesList.appendChild(fixtureItem);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染数据统计
|
||||
function renderStats() {
|
||||
renderScorers();
|
||||
renderAssists();
|
||||
renderTeamStats();
|
||||
renderScorers();
|
||||
renderAssists();
|
||||
renderTeamStats();
|
||||
}
|
||||
|
||||
function renderScorers() {
|
||||
const scorersContainer = document.querySelector('#scorers');
|
||||
|
||||
if (!scorersContainer) return;
|
||||
|
||||
scorersContainer.innerHTML = `
|
||||
const scorersContainer = document.querySelector("#scorers");
|
||||
|
||||
if (!scorersContainer) return;
|
||||
|
||||
scorersContainer.innerHTML = `
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -338,7 +348,8 @@ function renderScorers() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${leagueData.players.scorers.map(player => {
|
||||
${leagueData.players.scorers
|
||||
.map((player) => {
|
||||
const team = getTeamById(player.teamId);
|
||||
return `
|
||||
<tr>
|
||||
@ -350,18 +361,19 @@ function renderScorers() {
|
||||
<td class="stats-value">${player.matches}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAssists() {
|
||||
const assistsContainer = document.querySelector('#assists');
|
||||
|
||||
if (!assistsContainer) return;
|
||||
|
||||
assistsContainer.innerHTML = `
|
||||
const assistsContainer = document.querySelector("#assists");
|
||||
|
||||
if (!assistsContainer) return;
|
||||
|
||||
assistsContainer.innerHTML = `
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -374,7 +386,8 @@ function renderAssists() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${leagueData.players.assists.map(player => {
|
||||
${leagueData.players.assists
|
||||
.map((player) => {
|
||||
const team = getTeamById(player.teamId);
|
||||
return `
|
||||
<tr>
|
||||
@ -386,36 +399,41 @@ function renderAssists() {
|
||||
<td class="stats-value">${player.matches}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTeamStats() {
|
||||
const teamStatsContainer = document.querySelector('#teams');
|
||||
|
||||
if (!teamStatsContainer) return;
|
||||
|
||||
// 计算球队统计数据
|
||||
const teamStats = leagueData.standings.map(standing => {
|
||||
const team = getTeamById(standing.teamId);
|
||||
const goalsPerGame = (standing.goalsFor / standing.played).toFixed(2);
|
||||
const concededPerGame = (standing.goalsAgainst / standing.played).toFixed(2);
|
||||
|
||||
return {
|
||||
rank: standing.rank,
|
||||
team: team.name,
|
||||
goalsFor: standing.goalsFor,
|
||||
goalsAgainst: standing.goalsAgainst,
|
||||
goalDifference: standing.goalDifference,
|
||||
goalsPerGame,
|
||||
concededPerGame,
|
||||
cleanSheets: Math.floor(Math.random() * 5) // 模拟数据
|
||||
};
|
||||
}).sort((a, b) => a.rank - b.rank);
|
||||
|
||||
teamStatsContainer.innerHTML = `
|
||||
const teamStatsContainer = document.querySelector("#teams");
|
||||
|
||||
if (!teamStatsContainer) return;
|
||||
|
||||
// 计算球队统计数据
|
||||
const teamStats = leagueData.standings
|
||||
.map((standing) => {
|
||||
const team = getTeamById(standing.teamId);
|
||||
const goalsPerGame = (standing.goalsFor / standing.played).toFixed(2);
|
||||
const concededPerGame = (standing.goalsAgainst / standing.played).toFixed(
|
||||
2,
|
||||
);
|
||||
|
||||
return {
|
||||
rank: standing.rank,
|
||||
team: team.name,
|
||||
goalsFor: standing.goalsFor,
|
||||
goalsAgainst: standing.goalsAgainst,
|
||||
goalDifference: standing.goalDifference,
|
||||
goalsPerGame,
|
||||
concededPerGame,
|
||||
cleanSheets: Math.floor(Math.random() * 5), // 模拟数据
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.rank - b.rank);
|
||||
|
||||
teamStatsContainer.innerHTML = `
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -430,18 +448,22 @@ function renderTeamStats() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${teamStats.map(stat => `
|
||||
${teamStats
|
||||
.map(
|
||||
(stat) => `
|
||||
<tr>
|
||||
<td class="stats-rank">${stat.rank}</td>
|
||||
<td class="stats-player">${stat.team}</td>
|
||||
<td class="stats-value">${stat.goalsFor}</td>
|
||||
<td class="stats-value">${stat.goalsAgainst}</td>
|
||||
<td class="stats-value">${stat.goalDifference > 0 ? '+' : ''}${stat.goalDifference}</td>
|
||||
<td class="stats-value">${stat.goalDifference > 0 ? "+" : ""}${stat.goalDifference}</td>
|
||||
<td class="stats-value">${stat.goalsPerGame}</td>
|
||||
<td class="stats-value">${stat.concededPerGame}</td>
|
||||
<td class="stats-value">${stat.cleanSheets}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
@ -449,24 +471,24 @@ function renderTeamStats() {
|
||||
|
||||
// 渲染新闻动态
|
||||
function renderNews() {
|
||||
const newsGrid = document.querySelector('.news-grid');
|
||||
|
||||
if (!newsGrid) return;
|
||||
|
||||
newsGrid.innerHTML = '';
|
||||
|
||||
leagueData.news.forEach(newsItem => {
|
||||
const newsCard = document.createElement('div');
|
||||
newsCard.className = 'news-card';
|
||||
|
||||
const date = new Date(newsItem.date);
|
||||
const formattedDate = date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
newsCard.innerHTML = `
|
||||
const newsGrid = document.querySelector(".news-grid");
|
||||
|
||||
if (!newsGrid) return;
|
||||
|
||||
newsGrid.innerHTML = "";
|
||||
|
||||
leagueData.news.forEach((newsItem) => {
|
||||
const newsCard = document.createElement("div");
|
||||
newsCard.className = "news-card";
|
||||
|
||||
const date = new Date(newsItem.date);
|
||||
const formattedDate = date.toLocaleDateString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
newsCard.innerHTML = `
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, ${newsItem.imageColor} 0%, ${darkenColor(newsItem.imageColor, 20)} 100%);"></div>
|
||||
<div class="news-card-content">
|
||||
<span class="news-card-category">${newsItem.category}</span>
|
||||
@ -481,138 +503,143 @@ function renderNews() {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
newsCard.addEventListener('click', () => {
|
||||
alert(`查看新闻: ${newsItem.title}`);
|
||||
});
|
||||
|
||||
newsGrid.appendChild(newsCard);
|
||||
|
||||
newsCard.addEventListener("click", () => {
|
||||
alert(`查看新闻: ${newsItem.title}`);
|
||||
});
|
||||
|
||||
newsGrid.appendChild(newsCard);
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化标签页切换
|
||||
function initTabs() {
|
||||
// 赛程标签页
|
||||
const fixtureTabs = document.querySelectorAll('.fixtures-tabs .tab');
|
||||
const fixtureItems = document.querySelectorAll('.fixture-item');
|
||||
|
||||
fixtureTabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
// 更新活动标签
|
||||
fixtureTabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
|
||||
const roundFilter = tab.getAttribute('data-round');
|
||||
|
||||
// 这里可以根据筛选条件显示不同的赛程
|
||||
// 由于时间关系,这里只是简单的演示
|
||||
console.log(`筛选赛程: ${roundFilter}`);
|
||||
});
|
||||
// 赛程标签页
|
||||
const fixtureTabs = document.querySelectorAll(".fixtures-tabs .tab");
|
||||
const fixtureItems = document.querySelectorAll(".fixture-item");
|
||||
|
||||
fixtureTabs.forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
// 更新活动标签
|
||||
fixtureTabs.forEach((t) => t.classList.remove("active"));
|
||||
tab.classList.add("active");
|
||||
|
||||
const roundFilter = tab.getAttribute("data-round");
|
||||
|
||||
// 这里可以根据筛选条件显示不同的赛程
|
||||
// 由于时间关系,这里只是简单的演示
|
||||
console.log(`筛选赛程: ${roundFilter}`);
|
||||
});
|
||||
|
||||
// 数据统计标签页
|
||||
const statsTabs = document.querySelectorAll('.stats-tab');
|
||||
const statsContents = document.querySelectorAll('.stats-tab-content');
|
||||
|
||||
statsTabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const tabId = tab.getAttribute('data-tab');
|
||||
|
||||
// 更新活动标签
|
||||
statsTabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
|
||||
// 显示对应内容
|
||||
statsContents.forEach(content => {
|
||||
content.classList.remove('active');
|
||||
if (content.id === tabId) {
|
||||
content.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 数据统计标签页
|
||||
const statsTabs = document.querySelectorAll(".stats-tab");
|
||||
const statsContents = document.querySelectorAll(".stats-tab-content");
|
||||
|
||||
statsTabs.forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
const tabId = tab.getAttribute("data-tab");
|
||||
|
||||
// 更新活动标签
|
||||
statsTabs.forEach((t) => t.classList.remove("active"));
|
||||
tab.classList.add("active");
|
||||
|
||||
// 显示对应内容
|
||||
statsContents.forEach((content) => {
|
||||
content.classList.remove("active");
|
||||
if (content.id === tabId) {
|
||||
content.classList.add("active");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化移动端菜单
|
||||
function initMobileMenu() {
|
||||
const menuToggle = document.querySelector('.btn-menu-toggle');
|
||||
const navMenu = document.querySelector('.nav-menu');
|
||||
|
||||
if (menuToggle && navMenu) {
|
||||
menuToggle.addEventListener('click', () => {
|
||||
navMenu.classList.toggle('active');
|
||||
|
||||
// 更新菜单图标
|
||||
const icon = menuToggle.querySelector('i');
|
||||
if (navMenu.classList.contains('active')) {
|
||||
icon.className = 'fas fa-times';
|
||||
} else {
|
||||
icon.className = 'fas fa-bars';
|
||||
}
|
||||
});
|
||||
|
||||
// 点击菜单外区域关闭菜单
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!navMenu.contains(e.target) && !menuToggle.contains(e.target)) {
|
||||
navMenu.classList.remove('active');
|
||||
menuToggle.querySelector('i').className = 'fas fa-bars';
|
||||
}
|
||||
});
|
||||
}
|
||||
const menuToggle = document.querySelector(".btn-menu-toggle");
|
||||
const navMenu = document.querySelector(".nav-menu");
|
||||
|
||||
if (menuToggle && navMenu) {
|
||||
menuToggle.addEventListener("click", () => {
|
||||
navMenu.classList.toggle("active");
|
||||
|
||||
// 更新菜单图标
|
||||
const icon = menuToggle.querySelector("i");
|
||||
if (navMenu.classList.contains("active")) {
|
||||
icon.className = "fas fa-times";
|
||||
} else {
|
||||
icon.className = "fas fa-bars";
|
||||
}
|
||||
});
|
||||
|
||||
// 点击菜单外区域关闭菜单
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!navMenu.contains(e.target) && !menuToggle.contains(e.target)) {
|
||||
navMenu.classList.remove("active");
|
||||
menuToggle.querySelector("i").className = "fas fa-bars";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数:加深颜色
|
||||
function darkenColor(color, percent) {
|
||||
const num = parseInt(color.replace("#", ""), 16);
|
||||
const amt = Math.round(2.55 * percent);
|
||||
const R = (num >> 16) - amt;
|
||||
const G = (num >> 8 & 0x00FF) - amt;
|
||||
const B = (num & 0x0000FF) - amt;
|
||||
|
||||
return "#" + (
|
||||
0x1000000 +
|
||||
(R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
|
||||
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
|
||||
(B < 255 ? B < 1 ? 0 : B : 255)
|
||||
).toString(16).slice(1);
|
||||
const num = parseInt(color.replace("#", ""), 16);
|
||||
const amt = Math.round(2.55 * percent);
|
||||
const R = (num >> 16) - amt;
|
||||
const G = ((num >> 8) & 0x00ff) - amt;
|
||||
const B = (num & 0x0000ff) - amt;
|
||||
|
||||
return (
|
||||
"#" +
|
||||
(
|
||||
0x1000000 +
|
||||
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
|
||||
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
|
||||
(B < 255 ? (B < 1 ? 0 : B) : 255)
|
||||
)
|
||||
.toString(16)
|
||||
.slice(1)
|
||||
);
|
||||
}
|
||||
|
||||
// 工具函数:格式化日期(简写)
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
return `${month}月${day}日`;
|
||||
const date = new Date(dateString);
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
return `${month}月${day}日`;
|
||||
}
|
||||
|
||||
// 工具函数:根据ID获取球队信息
|
||||
function getTeamById(teamId) {
|
||||
return leagueData.teams.find(team => team.id === teamId);
|
||||
return leagueData.teams.find((team) => team.id === teamId);
|
||||
}
|
||||
|
||||
// 添加一些交互效果
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 为所有按钮添加点击效果
|
||||
const buttons = document.querySelectorAll('.btn');
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('mousedown', () => {
|
||||
button.style.transform = 'scale(0.95)';
|
||||
});
|
||||
|
||||
button.addEventListener('mouseup', () => {
|
||||
button.style.transform = '';
|
||||
});
|
||||
|
||||
button.addEventListener('mouseleave', () => {
|
||||
button.style.transform = '';
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// 为所有按钮添加点击效果
|
||||
const buttons = document.querySelectorAll(".btn");
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener("mousedown", () => {
|
||||
button.style.transform = "scale(0.95)";
|
||||
});
|
||||
|
||||
// 为卡片添加悬停效果
|
||||
const cards = document.querySelectorAll('.team-card, .news-card');
|
||||
cards.forEach(card => {
|
||||
card.addEventListener('mouseenter', () => {
|
||||
card.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease';
|
||||
});
|
||||
|
||||
button.addEventListener("mouseup", () => {
|
||||
button.style.transform = "";
|
||||
});
|
||||
});
|
||||
|
||||
button.addEventListener("mouseleave", () => {
|
||||
button.style.transform = "";
|
||||
});
|
||||
});
|
||||
|
||||
// 为卡片添加悬停效果
|
||||
const cards = document.querySelectorAll(".team-card, .news-card");
|
||||
cards.forEach((card) => {
|
||||
card.addEventListener("mouseenter", () => {
|
||||
card.style.transition = "transform 0.3s ease, box-shadow 0.3s ease";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,385 +1,533 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>2026 Horizons: Trends & Opportunities</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📈</text></svg>">
|
||||
</head>
|
||||
<body>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📈</text></svg>"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<div class="nav-brand">
|
||||
<span class="brand-icon">📈</span>
|
||||
<span class="brand-text">2026 Horizons</span>
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#overview">Overview</a></li>
|
||||
<li><a href="#trends">Trends</a></li>
|
||||
<li><a href="#opportunities">Opportunities</a></li>
|
||||
<li><a href="#challenges">Challenges</a></li>
|
||||
</ul>
|
||||
<button class="theme-toggle" id="themeToggle">
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
<div class="container">
|
||||
<div class="nav-brand">
|
||||
<span class="brand-icon">📈</span>
|
||||
<span class="brand-text">2026 Horizons</span>
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#overview">Overview</a></li>
|
||||
<li><a href="#trends">Trends</a></li>
|
||||
<li><a href="#opportunities">Opportunities</a></li>
|
||||
<li><a href="#challenges">Challenges</a></li>
|
||||
</ul>
|
||||
<button class="theme-toggle" id="themeToggle">
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<header class="hero">
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">Navigating the Future</h1>
|
||||
<p class="hero-subtitle">A comprehensive analysis of trends, opportunities, and challenges shaping 2026</p>
|
||||
<div class="hero-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-number">5</span>
|
||||
<span class="stat-label">Key Economic Trends</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-number">8</span>
|
||||
<span class="stat-label">High-Growth Markets</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-number">4</span>
|
||||
<span class="stat-label">Technology Shifts</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#trends" class="cta-button">Explore Trends <i class="fas fa-arrow-down"></i></a>
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">Navigating the Future</h1>
|
||||
<p class="hero-subtitle">
|
||||
A comprehensive analysis of trends, opportunities, and challenges
|
||||
shaping 2026
|
||||
</p>
|
||||
<div class="hero-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-number">5</span>
|
||||
<span class="stat-label">Key Economic Trends</span>
|
||||
</div>
|
||||
<div class="hero-visual">
|
||||
<div class="visual-element">
|
||||
<div class="circle"></div>
|
||||
<div class="line"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-number">8</span>
|
||||
<span class="stat-label">High-Growth Markets</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-number">4</span>
|
||||
<span class="stat-label">Technology Shifts</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#trends" class="cta-button"
|
||||
>Explore Trends <i class="fas fa-arrow-down"></i
|
||||
></a>
|
||||
</div>
|
||||
<div class="hero-visual">
|
||||
<div class="visual-element">
|
||||
<div class="circle"></div>
|
||||
<div class="line"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Overview Section -->
|
||||
<section class="section overview" id="overview">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">The 2026 Landscape</h2>
|
||||
<p class="section-subtitle">Convergence, complexity, and unprecedented opportunities</p>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<div class="overview-text">
|
||||
<p>2026 represents a pivotal inflection point where accelerating technological convergence meets economic realignment and emerging market opportunities. The year will be defined by the interplay of AI maturation, quantum computing practicality, and sustainable transformation.</p>
|
||||
<p>Organizations and individuals who can navigate this complexity while maintaining strategic agility will be best positioned to capitalize on emerging opportunities across technology, business, and sustainability sectors.</p>
|
||||
</div>
|
||||
<div class="overview-highlight">
|
||||
<div class="highlight-card">
|
||||
<div class="highlight-icon">
|
||||
<i class="fas fa-brain"></i>
|
||||
</div>
|
||||
<h3 class="highlight-title">AI Maturation</h3>
|
||||
<p class="highlight-text">Transition from experimentation to production deployment with autonomous agents</p>
|
||||
</div>
|
||||
<div class="highlight-card">
|
||||
<div class="highlight-icon">
|
||||
<i class="fas fa-leaf"></i>
|
||||
</div>
|
||||
<h3 class="highlight-title">Sustainability Focus</h3>
|
||||
<p class="highlight-text">Climate tech emerges as a dominant investment category with material financial implications</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">The 2026 Landscape</h2>
|
||||
<p class="section-subtitle">
|
||||
Convergence, complexity, and unprecedented opportunities
|
||||
</p>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<div class="overview-text">
|
||||
<p>
|
||||
2026 represents a pivotal inflection point where accelerating
|
||||
technological convergence meets economic realignment and emerging
|
||||
market opportunities. The year will be defined by the interplay of
|
||||
AI maturation, quantum computing practicality, and sustainable
|
||||
transformation.
|
||||
</p>
|
||||
<p>
|
||||
Organizations and individuals who can navigate this complexity
|
||||
while maintaining strategic agility will be best positioned to
|
||||
capitalize on emerging opportunities across technology, business,
|
||||
and sustainability sectors.
|
||||
</p>
|
||||
</div>
|
||||
<div class="overview-highlight">
|
||||
<div class="highlight-card">
|
||||
<div class="highlight-icon">
|
||||
<i class="fas fa-brain"></i>
|
||||
</div>
|
||||
<h3 class="highlight-title">AI Maturation</h3>
|
||||
<p class="highlight-text">
|
||||
Transition from experimentation to production deployment with
|
||||
autonomous agents
|
||||
</p>
|
||||
</div>
|
||||
<div class="highlight-card">
|
||||
<div class="highlight-icon">
|
||||
<i class="fas fa-leaf"></i>
|
||||
</div>
|
||||
<h3 class="highlight-title">Sustainability Focus</h3>
|
||||
<p class="highlight-text">
|
||||
Climate tech emerges as a dominant investment category with
|
||||
material financial implications
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Trends Section -->
|
||||
<section class="section trends" id="trends">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Key Trends Shaping 2026</h2>
|
||||
<p class="section-subtitle">Critical developments across technology, economy, and society</p>
|
||||
</div>
|
||||
|
||||
<div class="trends-grid">
|
||||
<!-- Technology Trends -->
|
||||
<div class="trend-category">
|
||||
<h3 class="category-title"><i class="fas fa-microchip"></i> Technology & Innovation</h3>
|
||||
<div class="trend-cards">
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge tech">AI</span>
|
||||
<span class="trend-priority high">High Impact</span>
|
||||
</div>
|
||||
<h4 class="trend-name">AI Agents Proliferation</h4>
|
||||
<p class="trend-description">Autonomous AI agents become mainstream in enterprise operations, requiring sophisticated governance frameworks and security considerations.</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"><i class="fas fa-rocket"></i> Exponential Growth</span>
|
||||
<span class="metric"><i class="fas fa-shield-alt"></i> Security Critical</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge tech">Quantum</span>
|
||||
<span class="trend-priority medium">Emerging</span>
|
||||
</div>
|
||||
<h4 class="trend-name">Quantum-AI Convergence</h4>
|
||||
<p class="trend-description">18% of global quantum algorithm revenues expected from AI applications, marking a significant shift toward practical quantum computing applications.</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"><i class="fas fa-chart-line"></i> 18% Revenue Share</span>
|
||||
<span class="metric"><i class="fas fa-cogs"></i> Optimization Focus</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge tech">Security</span>
|
||||
<span class="trend-priority high">Critical</span>
|
||||
</div>
|
||||
<h4 class="trend-name">AI-Powered Cybersecurity</h4>
|
||||
<p class="trend-description">Organizations leverage AI for threat detection, red teaming, and automated defense at machine speed, creating new security paradigms.</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"><i class="fas fa-bolt"></i> Machine Speed</span>
|
||||
<span class="metric"><i class="fas fa-user-shield"></i> Proactive Defense</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Economic Trends -->
|
||||
<div class="trend-category">
|
||||
<h3 class="category-title"><i class="fas fa-chart-line"></i> Economic & Global</h3>
|
||||
<div class="trend-cards">
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge econ">Finance</span>
|
||||
<span class="trend-priority high">Transformative</span>
|
||||
</div>
|
||||
<h4 class="trend-name">Tokenized Cross-Border Payments</h4>
|
||||
<p class="trend-description">Nearly 75% of G20 countries expected to have digital token payment systems, challenging traditional banking and dollar dominance.</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"><i class="fas fa-globe"></i> 75% G20 Adoption</span>
|
||||
<span class="metric"><i class="fas fa-exchange-alt"></i> Borderless</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge econ">Trade</span>
|
||||
<span class="trend-priority medium">Volatile</span>
|
||||
</div>
|
||||
<h4 class="trend-name">Trade Realignments</h4>
|
||||
<p class="trend-description">Continued US-China tensions with potential EU tariff responses on advanced manufacturing, reshaping global supply chains.</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"><i class="fas fa-balance-scale"></i> Geopolitical Shift</span>
|
||||
<span class="metric"><i class="fas fa-industry"></i> Supply Chain Impact</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge econ">Risk</span>
|
||||
<span class="trend-priority high">Critical</span>
|
||||
</div>
|
||||
<h4 class="trend-name">Debt Sustainability Challenges</h4>
|
||||
<p class="trend-description">Record public debt levels with limited fiscal restraint appetite as central banks unwind balance sheets.</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"><i class="fas fa-exclamation-triangle"></i> Record Levels</span>
|
||||
<span class="metric"><i class="fas fa-percentage"></i> Yield Pressure</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Key Trends Shaping 2026</h2>
|
||||
<p class="section-subtitle">
|
||||
Critical developments across technology, economy, and society
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="trends-grid">
|
||||
<!-- Technology Trends -->
|
||||
<div class="trend-category">
|
||||
<h3 class="category-title">
|
||||
<i class="fas fa-microchip"></i> Technology & Innovation
|
||||
</h3>
|
||||
<div class="trend-cards">
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge tech">AI</span>
|
||||
<span class="trend-priority high">High Impact</span>
|
||||
</div>
|
||||
<h4 class="trend-name">AI Agents Proliferation</h4>
|
||||
<p class="trend-description">
|
||||
Autonomous AI agents become mainstream in enterprise
|
||||
operations, requiring sophisticated governance frameworks and
|
||||
security considerations.
|
||||
</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"
|
||||
><i class="fas fa-rocket"></i> Exponential Growth</span
|
||||
>
|
||||
<span class="metric"
|
||||
><i class="fas fa-shield-alt"></i> Security Critical</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge tech">Quantum</span>
|
||||
<span class="trend-priority medium">Emerging</span>
|
||||
</div>
|
||||
<h4 class="trend-name">Quantum-AI Convergence</h4>
|
||||
<p class="trend-description">
|
||||
18% of global quantum algorithm revenues expected from AI
|
||||
applications, marking a significant shift toward practical
|
||||
quantum computing applications.
|
||||
</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"
|
||||
><i class="fas fa-chart-line"></i> 18% Revenue Share</span
|
||||
>
|
||||
<span class="metric"
|
||||
><i class="fas fa-cogs"></i> Optimization Focus</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge tech">Security</span>
|
||||
<span class="trend-priority high">Critical</span>
|
||||
</div>
|
||||
<h4 class="trend-name">AI-Powered Cybersecurity</h4>
|
||||
<p class="trend-description">
|
||||
Organizations leverage AI for threat detection, red teaming,
|
||||
and automated defense at machine speed, creating new security
|
||||
paradigms.
|
||||
</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"
|
||||
><i class="fas fa-bolt"></i> Machine Speed</span
|
||||
>
|
||||
<span class="metric"
|
||||
><i class="fas fa-user-shield"></i> Proactive Defense</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Economic Trends -->
|
||||
<div class="trend-category">
|
||||
<h3 class="category-title">
|
||||
<i class="fas fa-chart-line"></i> Economic & Global
|
||||
</h3>
|
||||
<div class="trend-cards">
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge econ">Finance</span>
|
||||
<span class="trend-priority high">Transformative</span>
|
||||
</div>
|
||||
<h4 class="trend-name">Tokenized Cross-Border Payments</h4>
|
||||
<p class="trend-description">
|
||||
Nearly 75% of G20 countries expected to have digital token
|
||||
payment systems, challenging traditional banking and dollar
|
||||
dominance.
|
||||
</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"
|
||||
><i class="fas fa-globe"></i> 75% G20 Adoption</span
|
||||
>
|
||||
<span class="metric"
|
||||
><i class="fas fa-exchange-alt"></i> Borderless</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge econ">Trade</span>
|
||||
<span class="trend-priority medium">Volatile</span>
|
||||
</div>
|
||||
<h4 class="trend-name">Trade Realignments</h4>
|
||||
<p class="trend-description">
|
||||
Continued US-China tensions with potential EU tariff responses
|
||||
on advanced manufacturing, reshaping global supply chains.
|
||||
</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"
|
||||
><i class="fas fa-balance-scale"></i> Geopolitical
|
||||
Shift</span
|
||||
>
|
||||
<span class="metric"
|
||||
><i class="fas fa-industry"></i> Supply Chain Impact</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trend-card">
|
||||
<div class="trend-header">
|
||||
<span class="trend-badge econ">Risk</span>
|
||||
<span class="trend-priority high">Critical</span>
|
||||
</div>
|
||||
<h4 class="trend-name">Debt Sustainability Challenges</h4>
|
||||
<p class="trend-description">
|
||||
Record public debt levels with limited fiscal restraint
|
||||
appetite as central banks unwind balance sheets.
|
||||
</p>
|
||||
<div class="trend-metrics">
|
||||
<span class="metric"
|
||||
><i class="fas fa-exclamation-triangle"></i> Record
|
||||
Levels</span
|
||||
>
|
||||
<span class="metric"
|
||||
><i class="fas fa-percentage"></i> Yield Pressure</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Opportunities Section -->
|
||||
<section class="section opportunities" id="opportunities">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Emerging Opportunities</h2>
|
||||
<p class="section-subtitle">High-growth markets and strategic investment areas</p>
|
||||
</div>
|
||||
|
||||
<div class="opportunities-grid">
|
||||
<div class="opportunity-card">
|
||||
<div class="opportunity-icon climate">
|
||||
<i class="fas fa-solar-panel"></i>
|
||||
</div>
|
||||
<h3 class="opportunity-title">Climate Technology</h3>
|
||||
<p class="opportunity-description">Home energy solutions, carbon capture, and sustainable infrastructure with massive growth potential.</p>
|
||||
<div class="opportunity-market">
|
||||
<span class="market-size">$162B+</span>
|
||||
<span class="market-label">by 2030</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opportunity-card">
|
||||
<div class="opportunity-icon health">
|
||||
<i class="fas fa-heartbeat"></i>
|
||||
</div>
|
||||
<h3 class="opportunity-title">Preventive Health</h3>
|
||||
<p class="opportunity-description">Personalized wellness, early intervention technologies, and digital health platforms.</p>
|
||||
<div class="opportunity-market">
|
||||
<span class="market-size">High Growth</span>
|
||||
<span class="market-label">Post-pandemic focus</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opportunity-card">
|
||||
<div class="opportunity-icon tech">
|
||||
<i class="fas fa-robot"></i>
|
||||
</div>
|
||||
<h3 class="opportunity-title">AI Consulting</h3>
|
||||
<p class="opportunity-description">Industry-specific AI implementation services and agentic AI platform development.</p>
|
||||
<div class="opportunity-market">
|
||||
<span class="market-size">Specialized</span>
|
||||
<span class="market-label">Enterprise demand</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opportunity-card">
|
||||
<div class="opportunity-icon food">
|
||||
<i class="fas fa-seedling"></i>
|
||||
</div>
|
||||
<h3 class="opportunity-title">Plant-Based Foods</h3>
|
||||
<p class="opportunity-description">Sustainable food alternatives with projected market growth toward $162 billion by 2030.</p>
|
||||
<div class="opportunity-market">
|
||||
<span class="market-size">$162B</span>
|
||||
<span class="market-label">Market potential</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opportunity-highlight">
|
||||
<div class="highlight-content">
|
||||
<h3 class="highlight-title">Strategic Investment Shift</h3>
|
||||
<p>Venture capital is diversifying geographically with emerging hubs in Lagos, Bucharest, Riyadh, and other non-traditional locations. Decentralized finance continues to innovate alternatives to traditional systems.</p>
|
||||
</div>
|
||||
<div class="highlight-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">75%</span>
|
||||
<span class="stat-label">G20 Digital Payments</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">18%</span>
|
||||
<span class="stat-label">Quantum-AI Revenue</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Emerging Opportunities</h2>
|
||||
<p class="section-subtitle">
|
||||
High-growth markets and strategic investment areas
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="opportunities-grid">
|
||||
<div class="opportunity-card">
|
||||
<div class="opportunity-icon climate">
|
||||
<i class="fas fa-solar-panel"></i>
|
||||
</div>
|
||||
<h3 class="opportunity-title">Climate Technology</h3>
|
||||
<p class="opportunity-description">
|
||||
Home energy solutions, carbon capture, and sustainable
|
||||
infrastructure with massive growth potential.
|
||||
</p>
|
||||
<div class="opportunity-market">
|
||||
<span class="market-size">$162B+</span>
|
||||
<span class="market-label">by 2030</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opportunity-card">
|
||||
<div class="opportunity-icon health">
|
||||
<i class="fas fa-heartbeat"></i>
|
||||
</div>
|
||||
<h3 class="opportunity-title">Preventive Health</h3>
|
||||
<p class="opportunity-description">
|
||||
Personalized wellness, early intervention technologies, and
|
||||
digital health platforms.
|
||||
</p>
|
||||
<div class="opportunity-market">
|
||||
<span class="market-size">High Growth</span>
|
||||
<span class="market-label">Post-pandemic focus</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opportunity-card">
|
||||
<div class="opportunity-icon tech">
|
||||
<i class="fas fa-robot"></i>
|
||||
</div>
|
||||
<h3 class="opportunity-title">AI Consulting</h3>
|
||||
<p class="opportunity-description">
|
||||
Industry-specific AI implementation services and agentic AI
|
||||
platform development.
|
||||
</p>
|
||||
<div class="opportunity-market">
|
||||
<span class="market-size">Specialized</span>
|
||||
<span class="market-label">Enterprise demand</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opportunity-card">
|
||||
<div class="opportunity-icon food">
|
||||
<i class="fas fa-seedling"></i>
|
||||
</div>
|
||||
<h3 class="opportunity-title">Plant-Based Foods</h3>
|
||||
<p class="opportunity-description">
|
||||
Sustainable food alternatives with projected market growth toward
|
||||
$162 billion by 2030.
|
||||
</p>
|
||||
<div class="opportunity-market">
|
||||
<span class="market-size">$162B</span>
|
||||
<span class="market-label">Market potential</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opportunity-highlight">
|
||||
<div class="highlight-content">
|
||||
<h3 class="highlight-title">Strategic Investment Shift</h3>
|
||||
<p>
|
||||
Venture capital is diversifying geographically with emerging hubs
|
||||
in Lagos, Bucharest, Riyadh, and other non-traditional locations.
|
||||
Decentralized finance continues to innovate alternatives to
|
||||
traditional systems.
|
||||
</p>
|
||||
</div>
|
||||
<div class="highlight-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">75%</span>
|
||||
<span class="stat-label">G20 Digital Payments</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">18%</span>
|
||||
<span class="stat-label">Quantum-AI Revenue</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Challenges Section -->
|
||||
<section class="section challenges" id="challenges">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Critical Challenges & Risks</h2>
|
||||
<p class="section-subtitle">Navigating complexity in an uncertain landscape</p>
|
||||
</div>
|
||||
|
||||
<div class="challenges-content">
|
||||
<div class="challenge-card">
|
||||
<div class="challenge-header">
|
||||
<span class="challenge-severity high">High Risk</span>
|
||||
<h3 class="challenge-title">AI Security Vulnerabilities</h3>
|
||||
</div>
|
||||
<p class="challenge-description">New attack vectors require comprehensive defense strategies as autonomous agents proliferate across organizations.</p>
|
||||
<div class="challenge-mitigation">
|
||||
<span class="mitigation-label">Mitigation:</span>
|
||||
<span class="mitigation-text">Robust governance frameworks and AI-native security protocols</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="challenge-card">
|
||||
<div class="challenge-header">
|
||||
<span class="challenge-severity medium">Medium Risk</span>
|
||||
<h3 class="challenge-title">Talent & Skills Gap</h3>
|
||||
</div>
|
||||
<p class="challenge-description">Rapid technological change outpacing workforce skill development, creating critical talent shortages.</p>
|
||||
<div class="challenge-mitigation">
|
||||
<span class="mitigation-label">Mitigation:</span>
|
||||
<span class="mitigation-text">Continuous upskilling programs and AI collaboration training</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="challenge-card">
|
||||
<div class="challenge-header">
|
||||
<span class="challenge-severity high">High Risk</span>
|
||||
<h3 class="challenge-title">Economic Volatility</h3>
|
||||
</div>
|
||||
<p class="challenge-description">Potential AI bubble concerns, trade fragmentation, and competing payment systems creating market uncertainty.</p>
|
||||
<div class="challenge-mitigation">
|
||||
<span class="mitigation-label">Mitigation:</span>
|
||||
<span class="mitigation-text">Diversified portfolios and agile business models</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategic-implications">
|
||||
<h3 class="implications-title">Strategic Implications</h3>
|
||||
<div class="implications-grid">
|
||||
<div class="implication">
|
||||
<h4>For Businesses</h4>
|
||||
<p>Success requires embracing AI as a core competency while maintaining robust cybersecurity. Companies that navigate the sustainability transition while leveraging emerging technologies gain competitive advantages.</p>
|
||||
</div>
|
||||
<div class="implication">
|
||||
<h4>For Investors</h4>
|
||||
<p>Opportunities exist in climate tech, digital transformation, and Asian markets, but require careful assessment of geopolitical risks and potential market corrections.</p>
|
||||
</div>
|
||||
<div class="implication">
|
||||
<h4>For Individuals</h4>
|
||||
<p>Continuous upskilling in AI collaboration, quantum computing awareness, and digital literacy will be essential for career resilience in the evolving landscape.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Critical Challenges & Risks</h2>
|
||||
<p class="section-subtitle">
|
||||
Navigating complexity in an uncertain landscape
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="challenges-content">
|
||||
<div class="challenge-card">
|
||||
<div class="challenge-header">
|
||||
<span class="challenge-severity high">High Risk</span>
|
||||
<h3 class="challenge-title">AI Security Vulnerabilities</h3>
|
||||
</div>
|
||||
<p class="challenge-description">
|
||||
New attack vectors require comprehensive defense strategies as
|
||||
autonomous agents proliferate across organizations.
|
||||
</p>
|
||||
<div class="challenge-mitigation">
|
||||
<span class="mitigation-label">Mitigation:</span>
|
||||
<span class="mitigation-text"
|
||||
>Robust governance frameworks and AI-native security
|
||||
protocols</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="challenge-card">
|
||||
<div class="challenge-header">
|
||||
<span class="challenge-severity medium">Medium Risk</span>
|
||||
<h3 class="challenge-title">Talent & Skills Gap</h3>
|
||||
</div>
|
||||
<p class="challenge-description">
|
||||
Rapid technological change outpacing workforce skill development,
|
||||
creating critical talent shortages.
|
||||
</p>
|
||||
<div class="challenge-mitigation">
|
||||
<span class="mitigation-label">Mitigation:</span>
|
||||
<span class="mitigation-text"
|
||||
>Continuous upskilling programs and AI collaboration
|
||||
training</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="challenge-card">
|
||||
<div class="challenge-header">
|
||||
<span class="challenge-severity high">High Risk</span>
|
||||
<h3 class="challenge-title">Economic Volatility</h3>
|
||||
</div>
|
||||
<p class="challenge-description">
|
||||
Potential AI bubble concerns, trade fragmentation, and competing
|
||||
payment systems creating market uncertainty.
|
||||
</p>
|
||||
<div class="challenge-mitigation">
|
||||
<span class="mitigation-label">Mitigation:</span>
|
||||
<span class="mitigation-text"
|
||||
>Diversified portfolios and agile business models</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategic-implications">
|
||||
<h3 class="implications-title">Strategic Implications</h3>
|
||||
<div class="implications-grid">
|
||||
<div class="implication">
|
||||
<h4>For Businesses</h4>
|
||||
<p>
|
||||
Success requires embracing AI as a core competency while
|
||||
maintaining robust cybersecurity. Companies that navigate the
|
||||
sustainability transition while leveraging emerging technologies
|
||||
gain competitive advantages.
|
||||
</p>
|
||||
</div>
|
||||
<div class="implication">
|
||||
<h4>For Investors</h4>
|
||||
<p>
|
||||
Opportunities exist in climate tech, digital transformation, and
|
||||
Asian markets, but require careful assessment of geopolitical
|
||||
risks and potential market corrections.
|
||||
</p>
|
||||
</div>
|
||||
<div class="implication">
|
||||
<h4>For Individuals</h4>
|
||||
<p>
|
||||
Continuous upskilling in AI collaboration, quantum computing
|
||||
awareness, and digital literacy will be essential for career
|
||||
resilience in the evolving landscape.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<span class="brand-icon">📈</span>
|
||||
<span class="brand-text">2026 Horizons</span>
|
||||
<p class="footer-description">An analysis of trends shaping the future landscape</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<div class="link-group">
|
||||
<h4 class="link-title">Trends</h4>
|
||||
<a href="#trends">Technology</a>
|
||||
<a href="#trends">Economic</a>
|
||||
<a href="#trends">Sustainability</a>
|
||||
</div>
|
||||
<div class="link-group">
|
||||
<h4 class="link-title">Opportunities</h4>
|
||||
<a href="#opportunities">Markets</a>
|
||||
<a href="#opportunities">Investments</a>
|
||||
<a href="#opportunities">Startups</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<span class="brand-icon">📈</span>
|
||||
<span class="brand-text">2026 Horizons</span>
|
||||
<p class="footer-description">
|
||||
An analysis of trends shaping the future landscape
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<div class="link-group">
|
||||
<h4 class="link-title">Trends</h4>
|
||||
<a href="#trends">Technology</a>
|
||||
<a href="#trends">Economic</a>
|
||||
<a href="#trends">Sustainability</a>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<div class="copyright">
|
||||
<p>© 2026 Horizons Analysis. Based on current research and expert predictions.</p>
|
||||
</div>
|
||||
<div class="deerflow-branding">
|
||||
<a href="https://deerflow.tech" target="_blank" class="deerflow-link">
|
||||
<span class="deerflow-icon">✦</span>
|
||||
<span class="deerflow-text">Created by Deerflow</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="link-group">
|
||||
<h4 class="link-title">Opportunities</h4>
|
||||
<a href="#opportunities">Markets</a>
|
||||
<a href="#opportunities">Investments</a>
|
||||
<a href="#opportunities">Startups</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<div class="copyright">
|
||||
<p>
|
||||
© 2026 Horizons Analysis. Based on current research and
|
||||
expert predictions.
|
||||
</p>
|
||||
</div>
|
||||
<div class="deerflow-branding">
|
||||
<a
|
||||
href="https://deerflow.tech"
|
||||
target="_blank"
|
||||
class="deerflow-link"
|
||||
>
|
||||
<span class="deerflow-icon">✦</span>
|
||||
<span class="deerflow-text">Created by Deerflow</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,156 +1,168 @@
|
||||
// 2026 Horizons - Interactive Features
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Theme Toggle
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const themeIcon = themeToggle.querySelector('i');
|
||||
|
||||
// Check for saved theme or prefer-color-scheme
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
themeIcon.className = 'fas fa-sun';
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Theme Toggle
|
||||
const themeToggle = document.getElementById("themeToggle");
|
||||
const themeIcon = themeToggle.querySelector("i");
|
||||
|
||||
// Check for saved theme or prefer-color-scheme
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
if (savedTheme === "dark" || (!savedTheme && prefersDark)) {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
themeIcon.className = "fas fa-sun";
|
||||
}
|
||||
|
||||
themeToggle.addEventListener("click", function () {
|
||||
const currentTheme = document.documentElement.getAttribute("data-theme");
|
||||
|
||||
if (currentTheme === "dark") {
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
themeIcon.className = "fas fa-moon";
|
||||
localStorage.setItem("theme", "light");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
themeIcon.className = "fas fa-sun";
|
||||
localStorage.setItem("theme", "dark");
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('click', function() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||
|
||||
if (currentTheme === 'dark') {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
themeIcon.className = 'fas fa-moon';
|
||||
localStorage.setItem('theme', 'light');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
themeIcon.className = 'fas fa-sun';
|
||||
localStorage.setItem('theme', 'dark');
|
||||
}
|
||||
});
|
||||
|
||||
// Smooth scroll for navigation links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = this.getAttribute('href');
|
||||
if (targetId === '#') return;
|
||||
|
||||
const targetElement = document.querySelector(targetId);
|
||||
if (targetElement) {
|
||||
const headerHeight = document.querySelector('.navbar').offsetHeight;
|
||||
const targetPosition = targetElement.offsetTop - headerHeight - 20;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Smooth scroll for navigation links
|
||||
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
|
||||
anchor.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = this.getAttribute("href");
|
||||
if (targetId === "#") return;
|
||||
|
||||
const targetElement = document.querySelector(targetId);
|
||||
if (targetElement) {
|
||||
const headerHeight = document.querySelector(".navbar").offsetHeight;
|
||||
const targetPosition = targetElement.offsetTop - headerHeight - 20;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Navbar scroll effect
|
||||
const navbar = document.querySelector('.navbar');
|
||||
let lastScrollTop = 0;
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
// Hide/show navbar on scroll
|
||||
if (scrollTop > lastScrollTop && scrollTop > 100) {
|
||||
navbar.style.transform = 'translateY(-100%)';
|
||||
} else {
|
||||
navbar.style.transform = 'translateY(0)';
|
||||
}
|
||||
|
||||
lastScrollTop = scrollTop;
|
||||
|
||||
// Add shadow when scrolled
|
||||
if (scrollTop > 10) {
|
||||
navbar.style.boxShadow = 'var(--shadow-md)';
|
||||
} else {
|
||||
navbar.style.boxShadow = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Animate elements on scroll
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('fade-in');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe elements to animate
|
||||
document.querySelectorAll('.trend-card, .opportunity-card, .challenge-card, .highlight-card').forEach(el => {
|
||||
observer.observe(el);
|
||||
});
|
||||
|
||||
// Stats counter animation
|
||||
const stats = document.querySelectorAll('.stat-number');
|
||||
|
||||
const statsObserver = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const stat = entry.target;
|
||||
const targetValue = parseInt(stat.textContent);
|
||||
let currentValue = 0;
|
||||
const increment = targetValue / 50;
|
||||
const duration = 1500;
|
||||
const stepTime = Math.floor(duration / 50);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
currentValue += increment;
|
||||
if (currentValue >= targetValue) {
|
||||
stat.textContent = targetValue;
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
stat.textContent = Math.floor(currentValue);
|
||||
}
|
||||
}, stepTime);
|
||||
|
||||
statsObserver.unobserve(stat);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.5 });
|
||||
|
||||
stats.forEach(stat => {
|
||||
statsObserver.observe(stat);
|
||||
});
|
||||
|
||||
// Hover effects for cards
|
||||
document.querySelectorAll('.trend-card, .opportunity-card, .challenge-card').forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
this.style.zIndex = '10';
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', function() {
|
||||
this.style.zIndex = '1';
|
||||
});
|
||||
});
|
||||
|
||||
// Current year in footer
|
||||
const currentYear = new Date().getFullYear();
|
||||
const yearElement = document.querySelector('.copyright p');
|
||||
if (yearElement) {
|
||||
yearElement.textContent = yearElement.textContent.replace('2026', currentYear);
|
||||
});
|
||||
|
||||
// Navbar scroll effect
|
||||
const navbar = document.querySelector(".navbar");
|
||||
let lastScrollTop = 0;
|
||||
|
||||
window.addEventListener("scroll", function () {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
// Hide/show navbar on scroll
|
||||
if (scrollTop > lastScrollTop && scrollTop > 100) {
|
||||
navbar.style.transform = "translateY(-100%)";
|
||||
} else {
|
||||
navbar.style.transform = "translateY(0)";
|
||||
}
|
||||
|
||||
// Initialize animations
|
||||
setTimeout(() => {
|
||||
document.body.style.opacity = '1';
|
||||
}, 100);
|
||||
|
||||
lastScrollTop = scrollTop;
|
||||
|
||||
// Add shadow when scrolled
|
||||
if (scrollTop > 10) {
|
||||
navbar.style.boxShadow = "var(--shadow-md)";
|
||||
} else {
|
||||
navbar.style.boxShadow = "none";
|
||||
}
|
||||
});
|
||||
|
||||
// Animate elements on scroll
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: "0px 0px -50px 0px",
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("fade-in");
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe elements to animate
|
||||
document
|
||||
.querySelectorAll(
|
||||
".trend-card, .opportunity-card, .challenge-card, .highlight-card",
|
||||
)
|
||||
.forEach((el) => {
|
||||
observer.observe(el);
|
||||
});
|
||||
|
||||
// Stats counter animation
|
||||
const stats = document.querySelectorAll(".stat-number");
|
||||
|
||||
const statsObserver = new IntersectionObserver(
|
||||
function (entries) {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const stat = entry.target;
|
||||
const targetValue = parseInt(stat.textContent);
|
||||
let currentValue = 0;
|
||||
const increment = targetValue / 50;
|
||||
const duration = 1500;
|
||||
const stepTime = Math.floor(duration / 50);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
currentValue += increment;
|
||||
if (currentValue >= targetValue) {
|
||||
stat.textContent = targetValue;
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
stat.textContent = Math.floor(currentValue);
|
||||
}
|
||||
}, stepTime);
|
||||
|
||||
statsObserver.unobserve(stat);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
stats.forEach((stat) => {
|
||||
statsObserver.observe(stat);
|
||||
});
|
||||
|
||||
// Hover effects for cards
|
||||
document
|
||||
.querySelectorAll(".trend-card, .opportunity-card, .challenge-card")
|
||||
.forEach((card) => {
|
||||
card.addEventListener("mouseenter", function () {
|
||||
this.style.zIndex = "10";
|
||||
});
|
||||
|
||||
card.addEventListener("mouseleave", function () {
|
||||
this.style.zIndex = "1";
|
||||
});
|
||||
});
|
||||
|
||||
// Current year in footer
|
||||
const currentYear = new Date().getFullYear();
|
||||
const yearElement = document.querySelector(".copyright p");
|
||||
if (yearElement) {
|
||||
yearElement.textContent = yearElement.textContent.replace(
|
||||
"2026",
|
||||
currentYear,
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize animations
|
||||
setTimeout(() => {
|
||||
document.body.style.opacity = "1";
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Add CSS for initial load
|
||||
const style = document.createElement('style');
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
body {
|
||||
opacity: 0;
|
||||
@ -172,4 +184,4 @@ style.textContent = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
document.head.appendChild(style);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# The Leica Master's Eye: Capturing the Decisive Moment in the Age of AI
|
||||
|
||||
*By DeerFlow 2.0 | January 28, 2026*
|
||||
_By DeerFlow 2.0 | January 28, 2026_
|
||||
|
||||
## The Enduring Legacy of Leica Street Photography
|
||||
|
||||
@ -13,20 +13,25 @@ Through extensive research into Leica photography characteristics and careful pr
|
||||
My research reveals several key characteristics that define Leica master photography:
|
||||
|
||||
### 1. The Decisive Moment Philosophy
|
||||
|
||||
Henri Cartier-Bresson famously described photography as "the simultaneous recognition, in a fraction of a second, of the significance of an event." This philosophy emphasizes perfect timing where all visual elements align to create meaning beyond the literal scene.
|
||||
|
||||
### 2. Rangefinder Discretion
|
||||
|
||||
Leica's compact rangefinder design allows photographers to become part of the scene rather than observers behind bulky equipment. The quiet shutter and manual focus encourage deliberate, thoughtful composition.
|
||||
|
||||
### 3. Lens Character
|
||||
|
||||
Leica lenses are renowned for their "creamy bokeh" (background blur), natural color rendering, and three-dimensional "pop." Each lens has distinct characteristics—from the clinical sharpness of Summicron lenses to the dreamy quality of Noctilux wide-open.
|
||||
|
||||
### 4. Film-Like Aesthetic
|
||||
|
||||
Even with digital Leicas, photographers often emulate film characteristics: natural grain, subtle color shifts, and a certain "organic" quality that avoids the sterile perfection of some digital photography.
|
||||
|
||||
## Three AI-Generated Leica Masterpieces
|
||||
|
||||
### Image 1: Parisian Decisive Moment
|
||||
|
||||

|
||||
|
||||
This image captures the essence of Cartier-Bresson's philosophy. A woman in a red coat leaps over a puddle while a cyclist passes in perfect synchrony. The composition follows the rule of thirds, with the subject positioned at the intersection of grid lines. Shot with a simulated Leica M11 and 35mm Summicron lens at f/2.8, the image features shallow depth of field, natural film grain, and the warm, muted color palette characteristic of Leica photography.
|
||||
@ -34,6 +39,7 @@ This image captures the essence of Cartier-Bresson's philosophy. A woman in a re
|
||||
The "decisive moment" here isn't just about timing—it's about the alignment of multiple elements: the woman's motion, the cyclist's position, the reflection in the puddle, and the directional morning light creating long shadows on wet cobblestones.
|
||||
|
||||
### Image 2: Tokyo Night Reflections
|
||||
|
||||

|
||||
|
||||
Moving to Shinjuku, Tokyo, this image explores the atmospheric possibilities of Leica's legendary Noctilux lens. Simulating a Leica M10-P with a 50mm f/0.95 Noctilux wide open, the image creates extremely shallow depth of field with beautiful bokeh balls from neon signs reflected in wet pavement.
|
||||
@ -41,6 +47,7 @@ Moving to Shinjuku, Tokyo, this image explores the atmospheric possibilities of
|
||||
A salaryman waits under glowing kanji signs, steam rising from a nearby ramen shop. The composition layers foreground reflection, mid-ground subject, and background neon glow to create depth and atmosphere. The color palette emphasizes cool blues and magentas with warm convenience store yellows—a classic Tokyo night aesthetic captured with Leica's cinematic sensibility.
|
||||
|
||||
### Image 3: New York City Candid
|
||||
|
||||

|
||||
|
||||
This Chinatown scene demonstrates the documentary power of Leica's Q2 camera with its fixed 28mm Summilux lens. The wide angle captures environmental context while maintaining intimate proximity to the subjects. A fishmonger hands a live fish to a customer while tourists photograph the scene—a moment of cultural contrast and authentic urban life.
|
||||
@ -52,18 +59,23 @@ The 28mm perspective shows multiple layers: the transaction in foreground, touri
|
||||
Creating these images required careful prompt engineering based on my research:
|
||||
|
||||
### Camera and Lens Specifications
|
||||
|
||||
Each prompt specified exact equipment:
|
||||
|
||||
- **Paris**: Leica M11 with 35mm f/2 Summicron at f/2.8
|
||||
- **Tokyo**: Leica M10-P with 50mm f/0.95 Noctilux at f/0.95
|
||||
- **NYC**: Leica Q2 with fixed 28mm f/1.7 Summilux at f/2.8
|
||||
|
||||
### Film Simulation
|
||||
|
||||
Different film stocks were simulated:
|
||||
|
||||
- Kodak Portra 400 for Paris (natural skin tones, fine grain)
|
||||
- Cinestill 800T for Tokyo (halation, cinematic look)
|
||||
- Kodak Ektar 100 for NYC (vibrant colors, fine grain)
|
||||
|
||||
### Composition Principles
|
||||
|
||||
- Rule of thirds positioning
|
||||
- Environmental storytelling
|
||||
- Layers of depth (foreground, mid-ground, background)
|
||||
@ -71,6 +83,7 @@ Different film stocks were simulated:
|
||||
- Negative space for breathing room
|
||||
|
||||
### Lighting Characteristics
|
||||
|
||||
- Natural, directional light sources
|
||||
- Practical lighting (neon signs, shop windows)
|
||||
- Atmospheric elements (rain, steam, smoke)
|
||||
@ -81,12 +94,14 @@ Different film stocks were simulated:
|
||||
These images demonstrate that AI can learn from photographic masters while creating original work. The key lies in understanding the principles behind the aesthetics—not just mimicking surface characteristics.
|
||||
|
||||
### What AI Gets Right:
|
||||
|
||||
- Technical accuracy (bokeh, depth of field, grain)
|
||||
- Composition principles
|
||||
- Lighting simulation
|
||||
- Environmental storytelling
|
||||
|
||||
### What Remains Human:
|
||||
|
||||
- Intentionality and concept development
|
||||
- Emotional connection to subjects
|
||||
- Ethical considerations in street photography
|
||||
@ -102,4 +117,4 @@ As AI continues to evolve, the most compelling work will likely come from those
|
||||
|
||||
---
|
||||
|
||||
*All images generated using structured prompt engineering based on Leica photography research. Prompts available upon request.*
|
||||
_All images generated using structured prompt engineering based on Leica photography research. Prompts available upon request._
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -738,4 +738,4 @@
|
||||
"interrupts": [],
|
||||
"checkpoint_id": "1f0fbee1-86cb-630e-8035-fdef3b9e7862",
|
||||
"parent_checkpoint_id": "1f0fbee1-86c7-6a6a-8034-0eba0e105137"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,264 +1,354 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Pride and Prejudice | Jane Austen</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400&family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400&family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="nav">
|
||||
<div class="nav-brand">P&P</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#about">About</a></li>
|
||||
<li><a href="#characters">Characters</a></li>
|
||||
<li><a href="#themes">Themes</a></li>
|
||||
<li><a href="#quotes">Quotes</a></li>
|
||||
</ul>
|
||||
<div class="nav-brand">P&P</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#about">About</a></li>
|
||||
<li><a href="#characters">Characters</a></li>
|
||||
<li><a href="#themes">Themes</a></li>
|
||||
<li><a href="#quotes">Quotes</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="hero-bg">
|
||||
<div class="hero-pattern"></div>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<p class="hero-subtitle">A Novel by</p>
|
||||
<h1 class="hero-title">
|
||||
<span class="title-line">Pride</span>
|
||||
<span class="title-ampersand">&</span>
|
||||
<span class="title-line">Prejudice</span>
|
||||
</h1>
|
||||
<p class="hero-author">Jane Austen</p>
|
||||
<p class="hero-year">1813</p>
|
||||
<div class="hero-divider">
|
||||
<span class="divider-line"></span>
|
||||
<span class="divider-ornament">❦</span>
|
||||
<span class="divider-line"></span>
|
||||
</div>
|
||||
<p class="hero-tagline">"It is a truth universally acknowledged..."</p>
|
||||
<a href="#about" class="hero-cta">
|
||||
<span>Discover the Story</span>
|
||||
<svg class="cta-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M12 5v14M5 12l7 7 7-7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="hero-scroll-indicator">
|
||||
<div class="scroll-line"></div>
|
||||
<div class="hero-bg">
|
||||
<div class="hero-pattern"></div>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<p class="hero-subtitle">A Novel by</p>
|
||||
<h1 class="hero-title">
|
||||
<span class="title-line">Pride</span>
|
||||
<span class="title-ampersand">&</span>
|
||||
<span class="title-line">Prejudice</span>
|
||||
</h1>
|
||||
<p class="hero-author">Jane Austen</p>
|
||||
<p class="hero-year">1813</p>
|
||||
<div class="hero-divider">
|
||||
<span class="divider-line"></span>
|
||||
<span class="divider-ornament">❦</span>
|
||||
<span class="divider-line"></span>
|
||||
</div>
|
||||
<p class="hero-tagline">"It is a truth universally acknowledged..."</p>
|
||||
<a href="#about" class="hero-cta">
|
||||
<span>Discover the Story</span>
|
||||
<svg
|
||||
class="cta-arrow"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path d="M12 5v14M5 12l7 7 7-7" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="hero-scroll-indicator">
|
||||
<div class="scroll-line"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- About Section -->
|
||||
<section id="about" class="about">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<span class="section-number">01</span>
|
||||
<h2 class="section-title">The Novel</h2>
|
||||
</div>
|
||||
<div class="about-content">
|
||||
<div class="about-text">
|
||||
<p class="about-lead">Set in rural England in the early 19th century, <em>Pride and Prejudice</em> tells the story of the Bennet family and their five unmarried daughters.</p>
|
||||
<p>When the wealthy and eligible Mr. Bingley rents a nearby estate, Mrs. Bennet sees an opportunity to marry off her eldest daughter, Jane. At a ball, Jane forms an attachment to Mr. Bingley, while her sister Elizabeth meets his friend, the proud Mr. Darcy.</p>
|
||||
<p>What follows is a masterful exploration of manners, morality, education, and marriage in the society of the landed gentry of early 19th-century England.</p>
|
||||
</div>
|
||||
<div class="about-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">61</span>
|
||||
<span class="stat-label">Chapters</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">122K</span>
|
||||
<span class="stat-label">Words</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">20M+</span>
|
||||
<span class="stat-label">Copies Sold</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<span class="section-number">01</span>
|
||||
<h2 class="section-title">The Novel</h2>
|
||||
</div>
|
||||
<div class="about-content">
|
||||
<div class="about-text">
|
||||
<p class="about-lead">
|
||||
Set in rural England in the early 19th century,
|
||||
<em>Pride and Prejudice</em> tells the story of the Bennet family
|
||||
and their five unmarried daughters.
|
||||
</p>
|
||||
<p>
|
||||
When the wealthy and eligible Mr. Bingley rents a nearby estate,
|
||||
Mrs. Bennet sees an opportunity to marry off her eldest daughter,
|
||||
Jane. At a ball, Jane forms an attachment to Mr. Bingley, while
|
||||
her sister Elizabeth meets his friend, the proud Mr. Darcy.
|
||||
</p>
|
||||
<p>
|
||||
What follows is a masterful exploration of manners, morality,
|
||||
education, and marriage in the society of the landed gentry of
|
||||
early 19th-century England.
|
||||
</p>
|
||||
</div>
|
||||
<div class="about-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">61</span>
|
||||
<span class="stat-label">Chapters</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">122K</span>
|
||||
<span class="stat-label">Words</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">20M+</span>
|
||||
<span class="stat-label">Copies Sold</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Characters Section -->
|
||||
<section id="characters" class="characters">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<span class="section-number">02</span>
|
||||
<h2 class="section-title">The Characters</h2>
|
||||
</div>
|
||||
<div class="characters-grid">
|
||||
<div class="character-card featured">
|
||||
<div class="character-portrait elizabeth"></div>
|
||||
<div class="character-info">
|
||||
<h3>Elizabeth Bennet</h3>
|
||||
<p class="character-role">The Protagonist</p>
|
||||
<p class="character-desc">Intelligent, witty, and independent, Elizabeth navigates society's expectations while staying true to her principles.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character-card featured">
|
||||
<div class="character-portrait darcy"></div>
|
||||
<div class="character-info">
|
||||
<h3>Fitzwilliam Darcy</h3>
|
||||
<p class="character-role">The Romantic Lead</p>
|
||||
<p class="character-desc">Wealthy, reserved, and initially perceived as arrogant, Darcy's true character is revealed through his actions.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character-card">
|
||||
<div class="character-portrait jane"></div>
|
||||
<div class="character-info">
|
||||
<h3>Jane Bennet</h3>
|
||||
<p class="character-role">The Eldest Sister</p>
|
||||
<p class="character-desc">Beautiful, gentle, and always sees the best in people.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character-card">
|
||||
<div class="character-portrait bingley"></div>
|
||||
<div class="character-info">
|
||||
<h3>Charles Bingley</h3>
|
||||
<p class="character-role">The Amiable Gentleman</p>
|
||||
<p class="character-desc">Wealthy, good-natured, and easily influenced by his friends.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character-card">
|
||||
<div class="character-portrait lydia"></div>
|
||||
<div class="character-info">
|
||||
<h3>Lydia Bennet</h3>
|
||||
<p class="character-role">The Youngest Sister</p>
|
||||
<p class="character-desc">Frivolous, flirtatious, and impulsive, causing family scandal.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character-card">
|
||||
<div class="character-portrait wickham"></div>
|
||||
<div class="character-info">
|
||||
<h3>George Wickham</h3>
|
||||
<p class="character-role">The Antagonist</p>
|
||||
<p class="character-desc">Charming on the surface but deceitful and manipulative.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<span class="section-number">02</span>
|
||||
<h2 class="section-title">The Characters</h2>
|
||||
</div>
|
||||
<div class="characters-grid">
|
||||
<div class="character-card featured">
|
||||
<div class="character-portrait elizabeth"></div>
|
||||
<div class="character-info">
|
||||
<h3>Elizabeth Bennet</h3>
|
||||
<p class="character-role">The Protagonist</p>
|
||||
<p class="character-desc">
|
||||
Intelligent, witty, and independent, Elizabeth navigates
|
||||
society's expectations while staying true to her principles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character-card featured">
|
||||
<div class="character-portrait darcy"></div>
|
||||
<div class="character-info">
|
||||
<h3>Fitzwilliam Darcy</h3>
|
||||
<p class="character-role">The Romantic Lead</p>
|
||||
<p class="character-desc">
|
||||
Wealthy, reserved, and initially perceived as arrogant, Darcy's
|
||||
true character is revealed through his actions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character-card">
|
||||
<div class="character-portrait jane"></div>
|
||||
<div class="character-info">
|
||||
<h3>Jane Bennet</h3>
|
||||
<p class="character-role">The Eldest Sister</p>
|
||||
<p class="character-desc">
|
||||
Beautiful, gentle, and always sees the best in people.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character-card">
|
||||
<div class="character-portrait bingley"></div>
|
||||
<div class="character-info">
|
||||
<h3>Charles Bingley</h3>
|
||||
<p class="character-role">The Amiable Gentleman</p>
|
||||
<p class="character-desc">
|
||||
Wealthy, good-natured, and easily influenced by his friends.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character-card">
|
||||
<div class="character-portrait lydia"></div>
|
||||
<div class="character-info">
|
||||
<h3>Lydia Bennet</h3>
|
||||
<p class="character-role">The Youngest Sister</p>
|
||||
<p class="character-desc">
|
||||
Frivolous, flirtatious, and impulsive, causing family scandal.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character-card">
|
||||
<div class="character-portrait wickham"></div>
|
||||
<div class="character-info">
|
||||
<h3>George Wickham</h3>
|
||||
<p class="character-role">The Antagonist</p>
|
||||
<p class="character-desc">
|
||||
Charming on the surface but deceitful and manipulative.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Themes Section -->
|
||||
<section id="themes" class="themes">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<span class="section-number">03</span>
|
||||
<h2 class="section-title">Themes</h2>
|
||||
</div>
|
||||
<div class="themes-content">
|
||||
<div class="theme-item">
|
||||
<div class="theme-icon">
|
||||
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20z"/>
|
||||
<path d="M24 12v20M16 24h16"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Pride</h3>
|
||||
<p>Darcy's pride in his social position initially prevents him from acknowledging his feelings for Elizabeth, while Elizabeth's pride in her discernment blinds her to Darcy's true character.</p>
|
||||
</div>
|
||||
<div class="theme-item">
|
||||
<div class="theme-icon">
|
||||
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M24 4l6 12 13 2-9 9 2 13-12-6-12 6 2-13-9-9 13-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Prejudice</h3>
|
||||
<p>Elizabeth's prejudice against Darcy, formed from their first meeting and Wickham's lies, nearly costs her happiness. The novel shows how first impressions can be misleading.</p>
|
||||
</div>
|
||||
<div class="theme-item">
|
||||
<div class="theme-icon">
|
||||
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20z"/>
|
||||
<path d="M14 24c0-5.523 4.477-10 10-10s10 4.477 10 10-4.477 10-10 10"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Marriage</h3>
|
||||
<p>The novel examines marriage from multiple perspectives: for love, for security, for social advancement, and the rare ideal of marrying for both love and compatibility.</p>
|
||||
</div>
|
||||
<div class="theme-item">
|
||||
<div class="theme-icon">
|
||||
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M12 12h24v24H12z"/>
|
||||
<path d="M12 12l24 24M36 12L12 36"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Class</h3>
|
||||
<p>The rigid class structure of Regency England shapes every interaction, from who may marry whom to how characters are judged by their connections and fortune.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<span class="section-number">03</span>
|
||||
<h2 class="section-title">Themes</h2>
|
||||
</div>
|
||||
<div class="themes-content">
|
||||
<div class="theme-item">
|
||||
<div class="theme-icon">
|
||||
<svg
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20z"
|
||||
/>
|
||||
<path d="M24 12v20M16 24h16" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Pride</h3>
|
||||
<p>
|
||||
Darcy's pride in his social position initially prevents him from
|
||||
acknowledging his feelings for Elizabeth, while Elizabeth's pride
|
||||
in her discernment blinds her to Darcy's true character.
|
||||
</p>
|
||||
</div>
|
||||
<div class="theme-item">
|
||||
<div class="theme-icon">
|
||||
<svg
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path d="M24 4l6 12 13 2-9 9 2 13-12-6-12 6 2-13-9-9 13-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Prejudice</h3>
|
||||
<p>
|
||||
Elizabeth's prejudice against Darcy, formed from their first
|
||||
meeting and Wickham's lies, nearly costs her happiness. The novel
|
||||
shows how first impressions can be misleading.
|
||||
</p>
|
||||
</div>
|
||||
<div class="theme-item">
|
||||
<div class="theme-icon">
|
||||
<svg
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20z"
|
||||
/>
|
||||
<path
|
||||
d="M14 24c0-5.523 4.477-10 10-10s10 4.477 10 10-4.477 10-10 10"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Marriage</h3>
|
||||
<p>
|
||||
The novel examines marriage from multiple perspectives: for love,
|
||||
for security, for social advancement, and the rare ideal of
|
||||
marrying for both love and compatibility.
|
||||
</p>
|
||||
</div>
|
||||
<div class="theme-item">
|
||||
<div class="theme-icon">
|
||||
<svg
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path d="M12 12h24v24H12z" />
|
||||
<path d="M12 12l24 24M36 12L12 36" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Class</h3>
|
||||
<p>
|
||||
The rigid class structure of Regency England shapes every
|
||||
interaction, from who may marry whom to how characters are judged
|
||||
by their connections and fortune.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Famous Quotes Section -->
|
||||
<section id="quotes" class="quotes">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<span class="section-number">04</span>
|
||||
<h2 class="section-title">Memorable Quotes</h2>
|
||||
</div>
|
||||
<div class="quotes-slider">
|
||||
<div class="quote-card active">
|
||||
<span class="quote-mark">"</span>
|
||||
<blockquote>It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.</blockquote>
|
||||
<cite>— Opening Line</cite>
|
||||
</div>
|
||||
<div class="quote-card">
|
||||
<span class="quote-mark">"</span>
|
||||
<blockquote>I could easily forgive his pride, if he had not mortified mine.</blockquote>
|
||||
<cite>— Elizabeth Bennet</cite>
|
||||
</div>
|
||||
<div class="quote-card">
|
||||
<span class="quote-mark">"</span>
|
||||
<blockquote>You have bewitched me, body and soul, and I love, I love, I love you.</blockquote>
|
||||
<cite>— Mr. Darcy</cite>
|
||||
</div>
|
||||
<div class="quote-card">
|
||||
<span class="quote-mark">"</span>
|
||||
<blockquote>Till this moment I never knew myself.</blockquote>
|
||||
<cite>— Elizabeth Bennet</cite>
|
||||
</div>
|
||||
<div class="quote-card">
|
||||
<span class="quote-mark">"</span>
|
||||
<blockquote>My good opinion once lost, is lost forever.</blockquote>
|
||||
<cite>— Mr. Darcy</cite>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quotes-nav">
|
||||
<button class="quote-dot active" data-index="0"></button>
|
||||
<button class="quote-dot" data-index="1"></button>
|
||||
<button class="quote-dot" data-index="2"></button>
|
||||
<button class="quote-dot" data-index="3"></button>
|
||||
<button class="quote-dot" data-index="4"></button>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<span class="section-number">04</span>
|
||||
<h2 class="section-title">Memorable Quotes</h2>
|
||||
</div>
|
||||
<div class="quotes-slider">
|
||||
<div class="quote-card active">
|
||||
<span class="quote-mark">"</span>
|
||||
<blockquote>
|
||||
It is a truth universally acknowledged, that a single man in
|
||||
possession of a good fortune, must be in want of a wife.
|
||||
</blockquote>
|
||||
<cite>— Opening Line</cite>
|
||||
</div>
|
||||
<div class="quote-card">
|
||||
<span class="quote-mark">"</span>
|
||||
<blockquote>
|
||||
I could easily forgive his pride, if he had not mortified mine.
|
||||
</blockquote>
|
||||
<cite>— Elizabeth Bennet</cite>
|
||||
</div>
|
||||
<div class="quote-card">
|
||||
<span class="quote-mark">"</span>
|
||||
<blockquote>
|
||||
You have bewitched me, body and soul, and I love, I love, I love
|
||||
you.
|
||||
</blockquote>
|
||||
<cite>— Mr. Darcy</cite>
|
||||
</div>
|
||||
<div class="quote-card">
|
||||
<span class="quote-mark">"</span>
|
||||
<blockquote>Till this moment I never knew myself.</blockquote>
|
||||
<cite>— Elizabeth Bennet</cite>
|
||||
</div>
|
||||
<div class="quote-card">
|
||||
<span class="quote-mark">"</span>
|
||||
<blockquote>My good opinion once lost, is lost forever.</blockquote>
|
||||
<cite>— Mr. Darcy</cite>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quotes-nav">
|
||||
<button class="quote-dot active" data-index="0"></button>
|
||||
<button class="quote-dot" data-index="1"></button>
|
||||
<button class="quote-dot" data-index="2"></button>
|
||||
<button class="quote-dot" data-index="3"></button>
|
||||
<button class="quote-dot" data-index="4"></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<span class="footer-logo">P&P</span>
|
||||
<p>A timeless masterpiece of English literature</p>
|
||||
</div>
|
||||
<div class="footer-divider">
|
||||
<span class="divider-ornament">❦</span>
|
||||
</div>
|
||||
<p class="footer-credit">Based on the 1813 novel by Jane Austen</p>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<span class="footer-logo">P&P</span>
|
||||
<p>A timeless masterpiece of English literature</p>
|
||||
</div>
|
||||
<div class="footer-divider">
|
||||
<span class="divider-ornament">❦</span>
|
||||
</div>
|
||||
<p class="footer-credit">Based on the 1813 novel by Jane Austen</p>
|
||||
</div>
|
||||
<a href="https://deerflow.tech" target="_blank" class="deerflow-signature">
|
||||
<span class="signature-text">Created By Deerflow</span>
|
||||
<span class="signature-icon">✦</span>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href="https://deerflow.tech"
|
||||
target="_blank"
|
||||
class="deerflow-signature"
|
||||
>
|
||||
<span class="signature-text">Created By Deerflow</span>
|
||||
<span class="signature-icon">✦</span>
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,177 +1,180 @@
|
||||
// Pride and Prejudice - Interactive Features
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Navigation scroll effect
|
||||
initNavigation();
|
||||
|
||||
// Quotes slider
|
||||
initQuotesSlider();
|
||||
|
||||
// Scroll reveal animations
|
||||
initScrollReveal();
|
||||
|
||||
// Smooth scroll for anchor links
|
||||
initSmoothScroll();
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Navigation scroll effect
|
||||
initNavigation();
|
||||
|
||||
// Quotes slider
|
||||
initQuotesSlider();
|
||||
|
||||
// Scroll reveal animations
|
||||
initScrollReveal();
|
||||
|
||||
// Smooth scroll for anchor links
|
||||
initSmoothScroll();
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// NAVIGATION SCROLL EFFECT
|
||||
// ============================================
|
||||
function initNavigation() {
|
||||
const nav = document.querySelector('.nav');
|
||||
let lastScroll = 0;
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
const currentScroll = window.pageYOffset;
|
||||
|
||||
// Add/remove scrolled class
|
||||
if (currentScroll > 100) {
|
||||
nav.classList.add('scrolled');
|
||||
} else {
|
||||
nav.classList.remove('scrolled');
|
||||
}
|
||||
|
||||
lastScroll = currentScroll;
|
||||
});
|
||||
const nav = document.querySelector(".nav");
|
||||
let lastScroll = 0;
|
||||
|
||||
window.addEventListener("scroll", () => {
|
||||
const currentScroll = window.pageYOffset;
|
||||
|
||||
// Add/remove scrolled class
|
||||
if (currentScroll > 100) {
|
||||
nav.classList.add("scrolled");
|
||||
} else {
|
||||
nav.classList.remove("scrolled");
|
||||
}
|
||||
|
||||
lastScroll = currentScroll;
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// QUOTES SLIDER
|
||||
// ============================================
|
||||
function initQuotesSlider() {
|
||||
const quotes = document.querySelectorAll('.quote-card');
|
||||
const dots = document.querySelectorAll('.quote-dot');
|
||||
let currentIndex = 0;
|
||||
let autoSlideInterval;
|
||||
|
||||
function showQuote(index) {
|
||||
// Remove active class from all quotes and dots
|
||||
quotes.forEach(quote => quote.classList.remove('active'));
|
||||
dots.forEach(dot => dot.classList.remove('active'));
|
||||
|
||||
// Add active class to current quote and dot
|
||||
quotes[index].classList.add('active');
|
||||
dots[index].classList.add('active');
|
||||
|
||||
currentIndex = index;
|
||||
}
|
||||
|
||||
function nextQuote() {
|
||||
const nextIndex = (currentIndex + 1) % quotes.length;
|
||||
showQuote(nextIndex);
|
||||
}
|
||||
|
||||
// Dot click handlers
|
||||
dots.forEach((dot, index) => {
|
||||
dot.addEventListener('click', () => {
|
||||
showQuote(index);
|
||||
resetAutoSlide();
|
||||
});
|
||||
const quotes = document.querySelectorAll(".quote-card");
|
||||
const dots = document.querySelectorAll(".quote-dot");
|
||||
let currentIndex = 0;
|
||||
let autoSlideInterval;
|
||||
|
||||
function showQuote(index) {
|
||||
// Remove active class from all quotes and dots
|
||||
quotes.forEach((quote) => quote.classList.remove("active"));
|
||||
dots.forEach((dot) => dot.classList.remove("active"));
|
||||
|
||||
// Add active class to current quote and dot
|
||||
quotes[index].classList.add("active");
|
||||
dots[index].classList.add("active");
|
||||
|
||||
currentIndex = index;
|
||||
}
|
||||
|
||||
function nextQuote() {
|
||||
const nextIndex = (currentIndex + 1) % quotes.length;
|
||||
showQuote(nextIndex);
|
||||
}
|
||||
|
||||
// Dot click handlers
|
||||
dots.forEach((dot, index) => {
|
||||
dot.addEventListener("click", () => {
|
||||
showQuote(index);
|
||||
resetAutoSlide();
|
||||
});
|
||||
|
||||
// Auto-slide functionality
|
||||
function startAutoSlide() {
|
||||
autoSlideInterval = setInterval(nextQuote, 6000);
|
||||
}
|
||||
|
||||
function resetAutoSlide() {
|
||||
clearInterval(autoSlideInterval);
|
||||
startAutoSlide();
|
||||
}
|
||||
|
||||
// Start auto-slide
|
||||
});
|
||||
|
||||
// Auto-slide functionality
|
||||
function startAutoSlide() {
|
||||
autoSlideInterval = setInterval(nextQuote, 6000);
|
||||
}
|
||||
|
||||
function resetAutoSlide() {
|
||||
clearInterval(autoSlideInterval);
|
||||
startAutoSlide();
|
||||
|
||||
// Pause on hover
|
||||
const slider = document.querySelector('.quotes-slider');
|
||||
slider.addEventListener('mouseenter', () => clearInterval(autoSlideInterval));
|
||||
slider.addEventListener('mouseleave', startAutoSlide);
|
||||
}
|
||||
|
||||
// Start auto-slide
|
||||
startAutoSlide();
|
||||
|
||||
// Pause on hover
|
||||
const slider = document.querySelector(".quotes-slider");
|
||||
slider.addEventListener("mouseenter", () => clearInterval(autoSlideInterval));
|
||||
slider.addEventListener("mouseleave", startAutoSlide);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SCROLL REVEAL ANIMATIONS
|
||||
// ============================================
|
||||
function initScrollReveal() {
|
||||
const revealElements = document.querySelectorAll(
|
||||
'.about-content, .character-card, .theme-item, .section-header'
|
||||
);
|
||||
|
||||
const revealOptions = {
|
||||
threshold: 0.15,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const revealObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry, index) => {
|
||||
if (entry.isIntersecting) {
|
||||
// Add staggered delay for grid items
|
||||
const delay = entry.target.classList.contains('character-card') ||
|
||||
entry.target.classList.contains('theme-item')
|
||||
? index * 100
|
||||
: 0;
|
||||
|
||||
setTimeout(() => {
|
||||
entry.target.classList.add('reveal');
|
||||
entry.target.style.opacity = '1';
|
||||
entry.target.style.transform = 'translateY(0)';
|
||||
}, delay);
|
||||
|
||||
revealObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, revealOptions);
|
||||
|
||||
revealElements.forEach(el => {
|
||||
el.style.opacity = '0';
|
||||
el.style.transform = 'translateY(30px)';
|
||||
el.style.transition = 'opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1), transform 0.8s cubic-bezier(0.16, 1, 0.3, 1)';
|
||||
revealObserver.observe(el);
|
||||
const revealElements = document.querySelectorAll(
|
||||
".about-content, .character-card, .theme-item, .section-header",
|
||||
);
|
||||
|
||||
const revealOptions = {
|
||||
threshold: 0.15,
|
||||
rootMargin: "0px 0px -50px 0px",
|
||||
};
|
||||
|
||||
const revealObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry, index) => {
|
||||
if (entry.isIntersecting) {
|
||||
// Add staggered delay for grid items
|
||||
const delay =
|
||||
entry.target.classList.contains("character-card") ||
|
||||
entry.target.classList.contains("theme-item")
|
||||
? index * 100
|
||||
: 0;
|
||||
|
||||
setTimeout(() => {
|
||||
entry.target.classList.add("reveal");
|
||||
entry.target.style.opacity = "1";
|
||||
entry.target.style.transform = "translateY(0)";
|
||||
}, delay);
|
||||
|
||||
revealObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, revealOptions);
|
||||
|
||||
revealElements.forEach((el) => {
|
||||
el.style.opacity = "0";
|
||||
el.style.transform = "translateY(30px)";
|
||||
el.style.transition =
|
||||
"opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1), transform 0.8s cubic-bezier(0.16, 1, 0.3, 1)";
|
||||
revealObserver.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SMOOTH SCROLL FOR ANCHOR LINKS
|
||||
// ============================================
|
||||
function initSmoothScroll() {
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
|
||||
if (target) {
|
||||
const navHeight = document.querySelector('.nav').offsetHeight;
|
||||
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - navHeight;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
|
||||
anchor.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute("href"));
|
||||
|
||||
if (target) {
|
||||
const navHeight = document.querySelector(".nav").offsetHeight;
|
||||
const targetPosition =
|
||||
target.getBoundingClientRect().top + window.pageYOffset - navHeight;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PARALLAX EFFECT FOR HERO
|
||||
// ============================================
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrolled = window.pageYOffset;
|
||||
const heroPattern = document.querySelector('.hero-pattern');
|
||||
|
||||
if (heroPattern && scrolled < window.innerHeight) {
|
||||
heroPattern.style.transform = `translateY(${scrolled * 0.3}px) rotate(${scrolled * 0.02}deg)`;
|
||||
}
|
||||
window.addEventListener("scroll", () => {
|
||||
const scrolled = window.pageYOffset;
|
||||
const heroPattern = document.querySelector(".hero-pattern");
|
||||
|
||||
if (heroPattern && scrolled < window.innerHeight) {
|
||||
heroPattern.style.transform = `translateY(${scrolled * 0.3}px) rotate(${scrolled * 0.02}deg)`;
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// CHARACTER CARD HOVER EFFECT
|
||||
// ============================================
|
||||
document.querySelectorAll('.character-card').forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
this.style.zIndex = '10';
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', function() {
|
||||
this.style.zIndex = '1';
|
||||
});
|
||||
document.querySelectorAll(".character-card").forEach((card) => {
|
||||
card.addEventListener("mouseenter", function () {
|
||||
this.style.zIndex = "10";
|
||||
});
|
||||
|
||||
card.addEventListener("mouseleave", function () {
|
||||
this.style.zIndex = "1";
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1187,9 +1187,7 @@
|
||||
{
|
||||
"name": "present_files",
|
||||
"args": {
|
||||
"filepaths": [
|
||||
"/mnt/user-data/outputs/diana_hu_research.md"
|
||||
]
|
||||
"filepaths": ["/mnt/user-data/outputs/diana_hu_research.md"]
|
||||
},
|
||||
"id": "call_fiwu3557asbu155mzi3ore72",
|
||||
"type": "tool_call"
|
||||
@ -1235,9 +1233,7 @@
|
||||
},
|
||||
"uploaded_files": [],
|
||||
"title": "Startup Tech Lead's Video Analysis Request",
|
||||
"artifacts": [
|
||||
"/mnt/user-data/outputs/diana_hu_research.md"
|
||||
],
|
||||
"artifacts": ["/mnt/user-data/outputs/diana_hu_research.md"],
|
||||
"todos": [
|
||||
{
|
||||
"content": "Analyze video transcript and extract key insights, advice, and frameworks",
|
||||
@ -1307,4 +1303,4 @@
|
||||
"interrupts": [],
|
||||
"checkpoint_id": "1f0f99e8-0d7c-62d2-8051-7cb8958145fe",
|
||||
"parent_checkpoint_id": "1f0f99e8-0437-6ac8-8050-ce8edd831bf1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# Diana Hu: Technical Startup Founder Advice - Comprehensive Research
|
||||
|
||||
## Video Overview
|
||||
|
||||
**Title:** Tips For Technical Startup Founders | Startup School
|
||||
**Speaker:** Diana Hu, Y Combinator Group Partner
|
||||
**Date:** April 21, 2023
|
||||
@ -10,11 +11,13 @@
|
||||
## Speaker Background
|
||||
|
||||
### Education
|
||||
|
||||
- **BS and MS in Electrical and Computer Engineering** from Carnegie Mellon University
|
||||
- Focus on **computer vision and machine learning**
|
||||
- Originally from Chile
|
||||
|
||||
### Career Path
|
||||
|
||||
1. **Co-founder & CTO of Escher Reality** (YC S17)
|
||||
- Startup building augmented reality SDK for game developers
|
||||
- Company acquired by Niantic (makers of Pokémon Go) in February 2018
|
||||
@ -29,11 +32,13 @@
|
||||
- Specializes in technical founder guidance
|
||||
|
||||
### Key Achievements
|
||||
|
||||
- Successfully built and sold AR startup to Niantic
|
||||
- Scaled systems from prototype to millions of users
|
||||
- Extensive experience mentoring technical founders
|
||||
|
||||
## Escher Reality Acquisition
|
||||
|
||||
- **Founded:** 2016
|
||||
- **Y Combinator Batch:** Summer 2017 (S17)
|
||||
- **Product:** Augmented Reality backend/SDK for cross-platform mobile AR
|
||||
@ -47,14 +52,17 @@
|
||||
### Three Stages of Technical Founder Journey
|
||||
|
||||
#### Stage 1: Ideating (0:00-8:30)
|
||||
|
||||
**Goal:** Build a prototype as soon as possible (matter of days)
|
||||
|
||||
**Key Principles:**
|
||||
|
||||
- Build something to show/demo to users
|
||||
- Doesn't have to work fully
|
||||
- CEO co-founder should be finding users to show prototype
|
||||
|
||||
**Examples:**
|
||||
|
||||
1. **Optimizely** (YC W10)
|
||||
- Built prototype in couple of days
|
||||
- JavaScript file on S3 for A/B testing
|
||||
@ -71,11 +79,13 @@
|
||||
- Enough to get users excited despite hard tech
|
||||
|
||||
**Common Mistakes:**
|
||||
|
||||
- Overbuilding at this stage
|
||||
- Not talking/listening to users soon enough
|
||||
- Getting too attached to initial ideas
|
||||
|
||||
#### Stage 2: Building MVP (8:30-19:43)
|
||||
|
||||
**Goal:** Build to launch quickly (weeks, not months)
|
||||
|
||||
**Key Principles:**
|
||||
@ -96,6 +106,7 @@
|
||||
- Don't build from scratch
|
||||
|
||||
**Examples:**
|
||||
|
||||
1. **DoorDash** (originally Palo Alto Delivery)
|
||||
- Static HTML with PDF menus
|
||||
- Google Forms for orders
|
||||
@ -114,12 +125,14 @@
|
||||
- Hired "misfits" overlooked by Google
|
||||
|
||||
**Tech Stack Philosophy:**
|
||||
|
||||
- "If you build a company and it works, tech choices don't matter as much"
|
||||
- Facebook: PHP → HipHop transpiler
|
||||
- JavaScript: V8 engine optimization
|
||||
- Choose what you're dangerous enough with
|
||||
|
||||
#### Stage 3: Launch Stage (19:43-26:51)
|
||||
|
||||
**Goal:** Iterate towards product-market fit
|
||||
|
||||
**Key Principles:**
|
||||
@ -140,6 +153,7 @@
|
||||
- Fix only what prevents product-market fit
|
||||
|
||||
**Examples:**
|
||||
|
||||
1. **WePay** (YC company)
|
||||
- Started as B2C payments (Venmo-like)
|
||||
- Analytics showed features unused
|
||||
@ -159,6 +173,7 @@
|
||||
- Added Node, PHP, WordPress support based on feedback
|
||||
|
||||
### Role Evolution Post Product-Market Fit
|
||||
|
||||
- **2-5 engineers:** 70% coding time
|
||||
- **5-10 engineers:** <50% coding time
|
||||
- **Beyond 10 engineers:** Little to no coding time
|
||||
@ -167,17 +182,20 @@
|
||||
## Key Concepts Deep Dive
|
||||
|
||||
### 90/10 Solution (Paul Buchheit)
|
||||
|
||||
- Find ways to get 90% of the value with 10% of the effort
|
||||
- Available 90% solution now is better than 100% solution later
|
||||
- Restrict product dimensions: geography, user type, data type, functionality
|
||||
|
||||
### Technical Debt in Startups
|
||||
|
||||
- **Early stage:** Embrace technical debt
|
||||
- **Post product-market fit:** Address scaling issues
|
||||
- **Philosophy:** "Tech debt is totally fine - feel the heat of your tech burning"
|
||||
- Only fix what prevents reaching product-market fit
|
||||
|
||||
### MVP Principles
|
||||
|
||||
1. **Speed over perfection:** Launch in weeks, not months
|
||||
2. **Manual processes:** Founders do unscalable work
|
||||
3. **Limited scope:** Constrain to prove core value
|
||||
@ -186,30 +204,35 @@
|
||||
## Companies Mentioned (with Context)
|
||||
|
||||
### Optimizely (YC W10)
|
||||
|
||||
- A/B testing platform
|
||||
- Prototype: JavaScript file on S3, manual execution
|
||||
- Founders: Pete Koomen and Dan Siroker
|
||||
- Dan previously headed analytics for Obama campaign
|
||||
|
||||
### Remora (YC W21)
|
||||
|
||||
- Carbon capture device for semi-trucks
|
||||
- Prototype: 3D renderings to demonstrate concept
|
||||
- Captures 80%+ of truck emissions
|
||||
- Can make trucks carbon-negative with biofuels
|
||||
|
||||
### Justin TV/Twitch
|
||||
|
||||
- Live streaming platform → gaming focus
|
||||
- Founders: Justin Kan, Emmett Shear, Michael Seibel, Kyle Vogt
|
||||
- MVP built by 4 founders (3 technical)
|
||||
- Hired overlooked engineers from Google
|
||||
|
||||
### Stripe
|
||||
|
||||
- Payment processing API
|
||||
- Early days: Founders manually processed payments
|
||||
- Filled bank forms manually for each transaction
|
||||
- Classic "do things that don't scale" example
|
||||
|
||||
### DoorDash
|
||||
|
||||
- Originally "Palo Alto Delivery"
|
||||
- Static HTML with PDF menus
|
||||
- Google Forms for orders
|
||||
@ -217,18 +240,21 @@
|
||||
- Focused on suburbs vs metro areas (competitive advantage)
|
||||
|
||||
### WayUp (YC 2015)
|
||||
|
||||
- Job board for college students
|
||||
- CTO JJ chose Django/Python over Ruby/Rails
|
||||
- Prioritized iteration speed over popular choice
|
||||
- Simple, effective tech stack
|
||||
|
||||
### WePay (YC company)
|
||||
|
||||
- Started as B2C payments (Venmo competitor)
|
||||
- Pivoted to API after user discovery
|
||||
- GoFundMe became key customer
|
||||
- Example of data + user interviews driving pivot
|
||||
|
||||
### Segment
|
||||
|
||||
- Analytics infrastructure
|
||||
- Multiple launches in short timeframe
|
||||
- Started with limited integrations
|
||||
@ -236,36 +262,42 @@
|
||||
- Acquired by Twilio for $3.2B
|
||||
|
||||
### Algolia
|
||||
|
||||
- Search API mentioned as YC success
|
||||
- Part of Diana's network of advised companies
|
||||
|
||||
## Actionable Advice for Technical Founders
|
||||
|
||||
### Immediate Actions (Week 1)
|
||||
|
||||
1. **Build clickable prototype** (Figma, InVision) in 1-3 days
|
||||
2. **Find 10 potential users** to show prototype
|
||||
3. **Use existing tools** rather than building from scratch
|
||||
4. **Embrace ugly code** - it's temporary
|
||||
|
||||
### Tech Stack Selection
|
||||
|
||||
1. **Choose familiarity over trendiness**
|
||||
2. **Use third-party services** for non-core functions
|
||||
3. **Keep infrastructure simple** (Heroku, Firebase, AWS)
|
||||
4. **Only build what's unique** to your value proposition
|
||||
|
||||
### Hiring Strategy
|
||||
|
||||
1. **Don't hire too early** (slows you down)
|
||||
2. **Founders must build** to gain product insights
|
||||
3. **Look for "misfits"** - overlooked talent
|
||||
4. **Post product-market fit:** Scale team strategically
|
||||
|
||||
### Launch Strategy
|
||||
|
||||
1. **Launch multiple times** (weekly iterations)
|
||||
2. **Combine analytics with user interviews**
|
||||
3. **Balance feature development with bug fixes**
|
||||
4. **Accept technical debt** until product-market fit
|
||||
|
||||
### Mindset Shifts
|
||||
|
||||
1. **From perfectionist to pragmatist**
|
||||
2. **From specialist to generalist** (do whatever it takes)
|
||||
3. **From employee to owner** (no task beneath you)
|
||||
@ -274,12 +306,14 @@
|
||||
## Diana's Personal Insights
|
||||
|
||||
### From Her Experience
|
||||
|
||||
- "Technical founder is committed to the success of your company"
|
||||
- "Do whatever it takes to get it to work"
|
||||
- "Your product will evolve - if someone else builds it, you miss key learnings"
|
||||
- "The only tech choices that matter are tied to customer promises"
|
||||
|
||||
### Common Traps to Avoid
|
||||
|
||||
1. **"What would Google do?"** - Building like a big company too early
|
||||
2. **Hiring to move faster** - Actually slows you down initially
|
||||
3. **Over-fixing vs building** - Focus on product-market fit first
|
||||
@ -288,12 +322,14 @@
|
||||
## Resources & References
|
||||
|
||||
### YC Resources
|
||||
|
||||
- Y Combinator Library: "Tips for technical startup founders"
|
||||
- Paul Graham Essay: "Do Things That Don't Scale"
|
||||
- Paul Buchheit Concept: "90/10 Solution"
|
||||
- Startup School: Technical founder track
|
||||
|
||||
### Tools Mentioned
|
||||
|
||||
- **Prototyping:** Figma, InVision
|
||||
- **Analytics:** Google Analytics, Amplitude, Mixpanel
|
||||
- **Infrastructure:** Heroku, Firebase, AWS, GCP
|
||||
@ -302,6 +338,7 @@
|
||||
- **Landing Pages:** Webflow
|
||||
|
||||
### Further Reading
|
||||
|
||||
1. Paul Graham essays (paulgraham.com)
|
||||
2. Y Combinator Startup School materials
|
||||
3. Case studies: Stripe, DoorDash, Segment early days
|
||||
@ -310,18 +347,21 @@
|
||||
## Key Takeaways
|
||||
|
||||
### For Technical Founders
|
||||
|
||||
1. **Speed is your superpower** - Move faster than established companies
|
||||
2. **Embrace imperfection** - Good enough beats perfect when speed matters
|
||||
3. **Stay close to users** - Insights come from conversations, not just data
|
||||
4. **Tech debt is a feature, not a bug** - Early stage startups should accumulate it
|
||||
|
||||
### For Startup Strategy
|
||||
|
||||
1. **Constrained focus** leads to better unit economics (DoorDash example)
|
||||
2. **Manual processes** create customer intimacy and learning
|
||||
3. **Continuous launching** builds momentum and feedback loops
|
||||
4. **Break things at scale** is a good problem to have
|
||||
|
||||
### For Team Building
|
||||
|
||||
1. **Founders build first** - Critical for product insights
|
||||
2. **Hire for adaptability** over pedigree
|
||||
3. **Evolve role with growth** - Coding time decreases with team size
|
||||
@ -329,4 +369,4 @@
|
||||
|
||||
---
|
||||
|
||||
*Research compiled from YouTube transcript, web searches, and Y Combinator resources. Last updated: January 25, 2026*
|
||||
_Research compiled from YouTube transcript, web searches, and Y Combinator resources. Last updated: January 25, 2026_
|
||||
|
||||
@ -113,9 +113,7 @@
|
||||
{
|
||||
"name": "present_files",
|
||||
"args": {
|
||||
"filepaths": [
|
||||
"/mnt/user-data/outputs/index.html"
|
||||
]
|
||||
"filepaths": ["/mnt/user-data/outputs/index.html"]
|
||||
},
|
||||
"id": "present_files:2",
|
||||
"type": "tool_call"
|
||||
@ -159,9 +157,7 @@
|
||||
},
|
||||
"uploaded_files": [],
|
||||
"title": "Interactive 3D Weather Forecast Interface",
|
||||
"artifacts": [
|
||||
"/mnt/user-data/outputs/index.html"
|
||||
]
|
||||
"artifacts": ["/mnt/user-data/outputs/index.html"]
|
||||
},
|
||||
"next": [],
|
||||
"tasks": [],
|
||||
@ -201,4 +197,4 @@
|
||||
"interrupts": [],
|
||||
"checkpoint_id": "1f0fbffa-a7ba-674e-800f-42a5653bc5dc",
|
||||
"parent_checkpoint_id": "1f0fbffa-9f3c-6e8c-800e-411520ded234"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1741,9 +1741,7 @@
|
||||
{
|
||||
"name": "present_files",
|
||||
"args": {
|
||||
"filepaths": [
|
||||
"/mnt/user-data/outputs/index.html"
|
||||
]
|
||||
"filepaths": ["/mnt/user-data/outputs/index.html"]
|
||||
},
|
||||
"id": "present_files:34",
|
||||
"type": "tool_call"
|
||||
@ -1873,4 +1871,4 @@
|
||||
"interrupts": [],
|
||||
"checkpoint_id": "1f0ff191-247b-6e28-8077-c6ae37cb0bc6",
|
||||
"parent_checkpoint_id": "1f0ff191-2479-6baa-8076-feaaf42a66ed"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -111,16 +111,16 @@ Open Issues: 196 (ongoing maintenance and feature development)
|
||||
|
||||
### Key Metrics
|
||||
|
||||
| Metric | Value | Assessment |
|
||||
|--------|-------|------------|
|
||||
| GitHub Stars | 19,531 | Exceptional popularity for research framework |
|
||||
| Forks | 2,452 | Strong community adoption and potential derivatives |
|
||||
| Contributors | 88 | Healthy open-source development ecosystem |
|
||||
| Open Issues | 196 | Active maintenance and feature development |
|
||||
| Primary Language | Python (1.29MB) | Main development language with extensive libraries |
|
||||
| Secondary Language | TypeScript (503KB) | Modern web UI implementation |
|
||||
| Repository Age | ~9 months | Rapid development and feature expansion |
|
||||
| License | MIT | Permissive open-source licensing |
|
||||
| Metric | Value | Assessment |
|
||||
| ------------------ | ------------------ | --------------------------------------------------- |
|
||||
| GitHub Stars | 19,531 | Exceptional popularity for research framework |
|
||||
| Forks | 2,452 | Strong community adoption and potential derivatives |
|
||||
| Contributors | 88 | Healthy open-source development ecosystem |
|
||||
| Open Issues | 196 | Active maintenance and feature development |
|
||||
| Primary Language | Python (1.29MB) | Main development language with extensive libraries |
|
||||
| Secondary Language | TypeScript (503KB) | Modern web UI implementation |
|
||||
| Repository Age | ~9 months | Rapid development and feature expansion |
|
||||
| License | MIT | Permissive open-source licensing |
|
||||
|
||||
---
|
||||
|
||||
@ -128,18 +128,18 @@ Open Issues: 196 (ongoing maintenance and feature development)
|
||||
|
||||
### Feature Comparison
|
||||
|
||||
| Feature | DeerFlow | OpenAI Deep Research | LangChain OpenDeepResearch |
|
||||
|---------|-----------|----------------------|----------------------------|
|
||||
| Multi-Agent Architecture | ✅ | ❌ | ✅ |
|
||||
| Local LLM Support | ✅ | ❌ | ✅ |
|
||||
| MCP Integration | ✅ | ❌ | ❌ |
|
||||
| Web Search Engines | Multiple (5+) | Limited | Limited |
|
||||
| Code Execution | ✅ Python REPL | Limited | ✅ |
|
||||
| Podcast Generation | ✅ | ❌ | ❌ |
|
||||
| Presentation Creation | ✅ | ❌ | ❌ |
|
||||
| Private Knowledgebase | ✅ (6+ options) | Limited | Limited |
|
||||
| Human-in-the-Loop | ✅ | Limited | ✅ |
|
||||
| Open Source | ✅ MIT | ❌ | ✅ Apache 2.0 |
|
||||
| Feature | DeerFlow | OpenAI Deep Research | LangChain OpenDeepResearch |
|
||||
| ------------------------ | --------------- | -------------------- | -------------------------- |
|
||||
| Multi-Agent Architecture | ✅ | ❌ | ✅ |
|
||||
| Local LLM Support | ✅ | ❌ | ✅ |
|
||||
| MCP Integration | ✅ | ❌ | ❌ |
|
||||
| Web Search Engines | Multiple (5+) | Limited | Limited |
|
||||
| Code Execution | ✅ Python REPL | Limited | ✅ |
|
||||
| Podcast Generation | ✅ | ❌ | ❌ |
|
||||
| Presentation Creation | ✅ | ❌ | ❌ |
|
||||
| Private Knowledgebase | ✅ (6+ options) | Limited | Limited |
|
||||
| Human-in-the-Loop | ✅ | Limited | ✅ |
|
||||
| Open Source | ✅ MIT | ❌ | ✅ Apache 2.0 |
|
||||
|
||||
### Market Positioning
|
||||
|
||||
@ -217,6 +217,7 @@ DeerFlow occupies a unique position in the deep research framework landscape by
|
||||
## Confidence Assessment
|
||||
|
||||
**High Confidence (90%+) Claims:**
|
||||
|
||||
- DeerFlow was created by ByteDance and open-sourced under MIT license in May 2025
|
||||
- The framework implements multi-agent architecture using LangGraph and LangChain
|
||||
- Current GitHub metrics: 19,531 stars, 2,452 forks, 88 contributors, 196 open issues
|
||||
@ -224,11 +225,13 @@ DeerFlow occupies a unique position in the deep research framework landscape by
|
||||
- Includes features for podcast generation, presentation creation, and human collaboration
|
||||
|
||||
**Medium Confidence (70-89%) Claims:**
|
||||
|
||||
- Specific performance benchmarks compared to proprietary alternatives
|
||||
- Detailed breakdown of enterprise adoption rates and use cases
|
||||
- Exact resource requirements for various deployment scenarios
|
||||
|
||||
**Lower Confidence (50-69%) Claims:**
|
||||
|
||||
- Future development roadmap beyond DeerFlow 2.0 transition
|
||||
- Specific enterprise customer implementations and case studies
|
||||
- Detailed comparison with emerging competitors not yet widely documented
|
||||
@ -255,4 +258,4 @@ This report was compiled using:
|
||||
**Report Prepared By:** Github Deep Research by DeerFlow
|
||||
**Date:** 2026-02-01
|
||||
**Report Version:** 1.0
|
||||
**Status:** Complete
|
||||
**Status:** Complete
|
||||
|
||||
@ -14,7 +14,8 @@ type MockThreadSearchResult = Record<string, unknown> & {
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = ((await request.json().catch(() => ({}))) ?? {}) as ThreadSearchRequest;
|
||||
const body = ((await request.json().catch(() => ({}))) ??
|
||||
{}) as ThreadSearchRequest;
|
||||
|
||||
const rawLimit = body.limit;
|
||||
let limit = 50;
|
||||
|
||||
@ -137,7 +137,10 @@ export default function ChatPage() {
|
||||
extraHeader={
|
||||
isNewThread && <Welcome mode={settings.context.mode} />
|
||||
}
|
||||
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || isUploading}
|
||||
disabled={
|
||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
|
||||
isUploading
|
||||
}
|
||||
onContextChange={(context) => setSettings("context", context)}
|
||||
onSubmit={handleSubmit}
|
||||
onStop={handleStop}
|
||||
|
||||
@ -19,7 +19,10 @@ export const Checkpoint = ({
|
||||
...props
|
||||
}: CheckpointProps) => (
|
||||
<div
|
||||
className={cn("flex items-center gap-0.5 text-muted-foreground overflow-hidden", className)}
|
||||
className={cn(
|
||||
"text-muted-foreground flex items-center gap-0.5 overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -115,7 +115,7 @@ export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
|
||||
<HoverCardTrigger asChild>
|
||||
{children ?? (
|
||||
<Button type="button" variant="ghost" {...props}>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
<span className="text-muted-foreground font-medium">
|
||||
{renderedPercent}
|
||||
</span>
|
||||
<ContextIcon />
|
||||
@ -163,7 +163,7 @@ export const ContextContentHeader = ({
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-3 text-xs">
|
||||
<p>{displayPct}</p>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
<p className="text-muted-foreground font-mono">
|
||||
{used} / {total}
|
||||
</p>
|
||||
</div>
|
||||
@ -213,8 +213,8 @@ export const ContextContentFooter = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-3 bg-secondary p-3 text-xs",
|
||||
className
|
||||
"bg-secondary flex w-full items-center justify-between gap-3 p-3 text-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -402,7 +402,7 @@ const TokensWithCost = ({
|
||||
notation: "compact",
|
||||
}).format(tokens)}
|
||||
{costText ? (
|
||||
<span className="ml-2 text-muted-foreground">• {costText}</span>
|
||||
<span className="text-muted-foreground ml-2">• {costText}</span>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
|
||||
@ -9,9 +9,9 @@ export type ControlsProps = ComponentProps<typeof ControlsPrimitive>;
|
||||
export const Controls = ({ className, ...props }: ControlsProps) => (
|
||||
<ControlsPrimitive
|
||||
className={cn(
|
||||
"gap-px overflow-hidden rounded-md border bg-card p-1 shadow-none!",
|
||||
"[&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent! [&>button]:hover:bg-secondary!",
|
||||
className
|
||||
"bg-card gap-px overflow-hidden rounded-md border p-1 shadow-none!",
|
||||
"[&>button]:hover:bg-secondary! [&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent!",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@ -29,7 +29,7 @@ const Temporary = ({
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
className="stroke-1 stroke-ring"
|
||||
className="stroke-ring stroke-1"
|
||||
id={id}
|
||||
path={edgePath}
|
||||
style={{
|
||||
@ -41,13 +41,13 @@ const Temporary = ({
|
||||
|
||||
const getHandleCoordsByPosition = (
|
||||
node: InternalNode<Node>,
|
||||
handlePosition: Position
|
||||
handlePosition: Position,
|
||||
) => {
|
||||
// Choose the handle type based on position - Left is for target, Right is for source
|
||||
const handleType = handlePosition === Position.Left ? "target" : "source";
|
||||
|
||||
const handle = node.internals.handleBounds?.[handleType]?.find(
|
||||
(h) => h.position === handlePosition
|
||||
(h) => h.position === handlePosition,
|
||||
);
|
||||
|
||||
if (!handle) {
|
||||
@ -85,7 +85,7 @@ const getHandleCoordsByPosition = (
|
||||
|
||||
const getEdgeParams = (
|
||||
source: InternalNode<Node>,
|
||||
target: InternalNode<Node>
|
||||
target: InternalNode<Node>,
|
||||
) => {
|
||||
const sourcePos = Position.Right;
|
||||
const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
|
||||
@ -112,7 +112,7 @@ const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
|
||||
|
||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
|
||||
sourceNode,
|
||||
targetNode
|
||||
targetNode,
|
||||
);
|
||||
|
||||
const [edgePath] = getBezierPath({
|
||||
|
||||
@ -17,7 +17,7 @@ export const Image = ({
|
||||
alt={props.alt}
|
||||
className={cn(
|
||||
"h-auto max-w-full overflow-hidden rounded-md",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
src={`data:${mediaType};base64,${base64}`}
|
||||
/>
|
||||
|
||||
@ -87,7 +87,7 @@ export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex animate-spin items-center justify-center",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@ -22,7 +22,7 @@ export const Node = ({ handles, className, ...props }: NodeProps) => (
|
||||
<Card
|
||||
className={cn(
|
||||
"node-container relative size-full h-auto w-sm gap-0 rounded-md p-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -36,7 +36,7 @@ export type NodeHeaderProps = ComponentProps<typeof CardHeader>;
|
||||
|
||||
export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
|
||||
<CardHeader
|
||||
className={cn("gap-0.5 rounded-t-md border-b bg-secondary p-3!", className)}
|
||||
className={cn("bg-secondary gap-0.5 rounded-t-md border-b p-3!", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@ -65,7 +65,7 @@ export type NodeFooterProps = ComponentProps<typeof CardFooter>;
|
||||
|
||||
export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
|
||||
<CardFooter
|
||||
className={cn("rounded-b-md border-t bg-secondary p-3!", className)}
|
||||
className={cn("bg-secondary rounded-b-md border-t p-3!", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -7,8 +7,8 @@ type PanelProps = ComponentProps<typeof PanelPrimitive>;
|
||||
export const Panel = ({ className, ...props }: PanelProps) => (
|
||||
<PanelPrimitive
|
||||
className={cn(
|
||||
"m-4 overflow-hidden rounded-md border bg-card p-1",
|
||||
className
|
||||
"bg-card m-4 overflow-hidden rounded-md border p-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@ -1163,7 +1163,8 @@ export const PromptInputSpeechButton = ({
|
||||
}
|
||||
|
||||
const currentTextareaRef = callbacksRef.current.textareaRef;
|
||||
const currentOnTranscriptionChange = callbacksRef.current.onTranscriptionChange;
|
||||
const currentOnTranscriptionChange =
|
||||
callbacksRef.current.onTranscriptionChange;
|
||||
|
||||
if (finalTranscript && currentTextareaRef?.current) {
|
||||
const textarea = currentTextareaRef.current;
|
||||
|
||||
@ -36,8 +36,8 @@ export type QueueItemProps = ComponentProps<"li">;
|
||||
export const QueueItem = ({ className, ...props }: QueueItemProps) => (
|
||||
<li
|
||||
className={cn(
|
||||
"group flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors hover:bg-muted",
|
||||
className
|
||||
"group hover:bg-muted flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -58,7 +58,7 @@ export const QueueItemIndicator = ({
|
||||
completed
|
||||
? "border-muted-foreground/20 bg-muted-foreground/10"
|
||||
: "border-muted-foreground/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -79,7 +79,7 @@ export const QueueItemContent = ({
|
||||
completed
|
||||
? "text-muted-foreground/50 line-through"
|
||||
: "text-muted-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -100,7 +100,7 @@ export const QueueItemDescription = ({
|
||||
completed
|
||||
? "text-muted-foreground/40 line-through"
|
||||
: "text-muted-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -126,8 +126,8 @@ export const QueueItemAction = ({
|
||||
}: QueueItemActionProps) => (
|
||||
<Button
|
||||
className={cn(
|
||||
"size-auto rounded p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-muted-foreground/10 hover:text-foreground group-hover:opacity-100",
|
||||
className
|
||||
"text-muted-foreground hover:bg-muted-foreground/10 hover:text-foreground size-auto rounded p-1 opacity-0 transition-opacity group-hover:opacity-100",
|
||||
className,
|
||||
)}
|
||||
size="icon"
|
||||
type="button"
|
||||
@ -169,8 +169,8 @@ export const QueueItemFile = ({
|
||||
}: QueueItemFileProps) => (
|
||||
<span
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded border bg-muted px-2 py-1 text-xs",
|
||||
className
|
||||
"bg-muted flex items-center gap-1 rounded border px-2 py-1 text-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -186,7 +186,7 @@ export const QueueList = ({
|
||||
className,
|
||||
...props
|
||||
}: QueueListProps) => (
|
||||
<ScrollArea className={cn("-mb-1 mt-2", className)} {...props}>
|
||||
<ScrollArea className={cn("mt-2 -mb-1", className)} {...props}>
|
||||
<div className="max-h-40 pr-4">
|
||||
<ul>{children}</ul>
|
||||
</div>
|
||||
@ -215,8 +215,8 @@ export const QueueSectionTrigger = ({
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
"group flex w-full items-center justify-between rounded-md bg-muted/40 px-3 py-2 text-left font-medium text-muted-foreground text-sm transition-colors hover:bg-muted",
|
||||
className
|
||||
"group bg-muted/40 text-muted-foreground hover:bg-muted flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm font-medium transition-colors",
|
||||
className,
|
||||
)}
|
||||
type="button"
|
||||
{...props}
|
||||
@ -241,7 +241,7 @@ export const QueueSectionLabel = ({
|
||||
...props
|
||||
}: QueueSectionLabelProps) => (
|
||||
<span className={cn("flex items-center gap-2", className)} {...props}>
|
||||
<ChevronDownIcon className="group-data-[state=closed]:-rotate-90 size-4 transition-transform" />
|
||||
<ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" />
|
||||
{icon}
|
||||
<span>
|
||||
{count} {label}
|
||||
@ -266,8 +266,8 @@ export type QueueProps = ComponentProps<"div">;
|
||||
export const Queue = ({ className, ...props }: QueueProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-2 rounded-xl border border-border bg-background px-3 pt-2 pb-2 shadow-xs",
|
||||
className
|
||||
"border-border bg-background flex flex-col gap-2 rounded-xl border px-3 pt-2 pb-2 shadow-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@ -108,10 +108,12 @@ export const Reasoning = memo(
|
||||
</Collapsible>
|
||||
</ReasoningContext.Provider>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
||||
export type ReasoningTriggerProps = ComponentProps<
|
||||
typeof CollapsibleTrigger
|
||||
> & {
|
||||
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
|
||||
};
|
||||
|
||||
@ -126,14 +128,19 @@ const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
||||
};
|
||||
|
||||
export const ReasoningTrigger = memo(
|
||||
({ className, children, getThinkingMessage = defaultGetThinkingMessage, ...props }: ReasoningTriggerProps) => {
|
||||
({
|
||||
className,
|
||||
children,
|
||||
getThinkingMessage = defaultGetThinkingMessage,
|
||||
...props
|
||||
}: ReasoningTriggerProps) => {
|
||||
const { isStreaming, isOpen, duration } = useReasoning();
|
||||
|
||||
return (
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
|
||||
className
|
||||
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -144,14 +151,14 @@ export const ReasoningTrigger = memo(
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
"size-4 transition-transform",
|
||||
isOpen ? "rotate-180" : "rotate-0"
|
||||
isOpen ? "rotate-180" : "rotate-0",
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export type ReasoningContentProps = ComponentProps<
|
||||
@ -165,14 +172,14 @@ export const ReasoningContent = memo(
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
"mt-4 text-sm",
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Streamdown {...props}>{children}</Streamdown>
|
||||
</CollapsibleContent>
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
Reasoning.displayName = "Reasoning";
|
||||
|
||||
@ -26,12 +26,12 @@ const ShimmerComponent = ({
|
||||
spread = 2,
|
||||
}: TextShimmerProps) => {
|
||||
const MotionComponent = motion.create(
|
||||
Component as keyof JSX.IntrinsicElements
|
||||
Component as keyof JSX.IntrinsicElements,
|
||||
);
|
||||
|
||||
const dynamicSpread = useMemo(
|
||||
() => (children?.length ?? 0) * spread,
|
||||
[children, spread]
|
||||
[children, spread],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -39,8 +39,8 @@ const ShimmerComponent = ({
|
||||
animate={{ backgroundPosition: "0% center" }}
|
||||
className={cn(
|
||||
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
|
||||
"[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]",
|
||||
className
|
||||
"[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))]",
|
||||
className,
|
||||
)}
|
||||
initial={{ backgroundPosition: "100% center" }}
|
||||
style={
|
||||
|
||||
@ -13,7 +13,7 @@ export type SourcesProps = ComponentProps<"div">;
|
||||
|
||||
export const Sources = ({ className, ...props }: SourcesProps) => (
|
||||
<Collapsible
|
||||
className={cn("not-prose mb-4 text-primary text-xs", className)}
|
||||
className={cn("not-prose text-primary mb-4 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@ -50,8 +50,8 @@ export const SourcesContent = ({
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
"mt-3 flex w-fit flex-col gap-2",
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@ -18,8 +18,8 @@ export const TaskItemFile = ({
|
||||
}: TaskItemFileProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-foreground text-xs",
|
||||
className
|
||||
"bg-secondary text-foreground inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -57,7 +57,7 @@ export const TaskTrigger = ({
|
||||
}: TaskTriggerProps) => (
|
||||
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
|
||||
{children ?? (
|
||||
<div className="flex w-full cursor-pointer items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground">
|
||||
<div className="text-muted-foreground hover:text-foreground flex w-full cursor-pointer items-center gap-2 text-sm transition-colors">
|
||||
<SearchIcon className="size-4" />
|
||||
<p className="text-sm">{title}</p>
|
||||
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
||||
@ -75,12 +75,12 @@ export const TaskContent = ({
|
||||
}: TaskContentProps) => (
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mt-4 space-y-2 border-muted border-l-2 pl-4">
|
||||
<div className="border-muted mt-4 space-y-2 border-l-2 pl-4">
|
||||
{children}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
|
||||
@ -7,8 +7,8 @@ type ToolbarProps = ComponentProps<typeof NodeToolbar>;
|
||||
export const Toolbar = ({ className, ...props }: ToolbarProps) => (
|
||||
<NodeToolbar
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded-sm border bg-background p-1.5",
|
||||
className
|
||||
"bg-background flex items-center gap-1 rounded-sm border p-1.5",
|
||||
className,
|
||||
)}
|
||||
position={Position.Bottom}
|
||||
{...props}
|
||||
|
||||
@ -66,8 +66,8 @@ export const WebPreview = ({
|
||||
<WebPreviewContext.Provider value={contextValue}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-full flex-col rounded-lg border bg-card",
|
||||
className
|
||||
"bg-card flex size-full flex-col rounded-lg border",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -107,7 +107,7 @@ export const WebPreviewNavigationButton = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className="h-8 w-8 p-0 hover:text-foreground"
|
||||
className="hover:text-foreground h-8 w-8 p-0"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
size="sm"
|
||||
@ -209,21 +209,21 @@ export const WebPreviewConsole = ({
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
className={cn("border-t bg-muted/50 font-mono text-sm", className)}
|
||||
className={cn("bg-muted/50 border-t font-mono text-sm", className)}
|
||||
onOpenChange={setConsoleOpen}
|
||||
open={consoleOpen}
|
||||
{...props}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
className="flex w-full items-center justify-between p-4 text-left font-medium hover:bg-muted/50"
|
||||
className="hover:bg-muted/50 flex w-full items-center justify-between p-4 text-left font-medium"
|
||||
variant="ghost"
|
||||
>
|
||||
Console
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
"h-4 w-4 transition-transform duration-200",
|
||||
consoleOpen && "rotate-180"
|
||||
consoleOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
@ -231,7 +231,7 @@ export const WebPreviewConsole = ({
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
"px-4 pb-4",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in"
|
||||
"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">
|
||||
@ -244,7 +244,7 @@ export const WebPreviewConsole = ({
|
||||
"text-xs",
|
||||
log.level === "error" && "text-destructive",
|
||||
log.level === "warn" && "text-yellow-600",
|
||||
log.level === "log" && "text-foreground"
|
||||
log.level === "log" && "text-foreground",
|
||||
)}
|
||||
key={`${log.timestamp.getTime()}-${index}`}
|
||||
>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
@ -16,8 +16,8 @@ const alertVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
@ -31,7 +31,7 @@ function Alert({
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -40,11 +40,11 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
@ -56,11 +56,11 @@ function AlertDescription({
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import React, { memo } from "react"
|
||||
import React, { memo } from "react";
|
||||
|
||||
interface AuroraTextProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
colors?: string[]
|
||||
speed?: number
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
colors?: string[];
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
export const AuroraText = memo(
|
||||
@ -23,7 +23,7 @@ export const AuroraText = memo(
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
animationDuration: `${10 / speed}s`,
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`relative inline-block ${className}`}>
|
||||
@ -36,8 +36,8 @@ export const AuroraText = memo(
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
AuroraText.displayName = "AuroraText"
|
||||
AuroraText.displayName = "AuroraText";
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
@ -14,11 +14,11 @@ function Avatar({
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
@ -31,7 +31,7 @@ function AvatarImage({
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
@ -43,11 +43,11 @@ function AvatarFallback({
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
@ -22,8 +22,8 @@ const badgeVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
@ -32,7 +32,7 @@ function Badge({
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@ -40,7 +40,7 @@ function Badge({
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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">) {
|
||||
@ -14,11 +14,11 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
@ -28,7 +28,7 @@ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
@ -36,9 +36,9 @@ function BreadcrumbLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@ -46,7 +46,7 @@ function BreadcrumbLink({
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
@ -59,7 +59,7 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
@ -77,7 +77,7 @@ function BreadcrumbSeparator({
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
@ -95,7 +95,7 @@ function BreadcrumbEllipsis({
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -106,4 +106,4 @@ export {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
const buttonGroupVariants = cva(
|
||||
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
||||
@ -18,8 +18,8 @@ const buttonGroupVariants = cva(
|
||||
defaultVariants: {
|
||||
orientation: "horizontal",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function ButtonGroup({
|
||||
className,
|
||||
@ -34,7 +34,7 @@ function ButtonGroup({
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroupText({
|
||||
@ -42,19 +42,19 @@ function ButtonGroupText({
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroupSeparator({
|
||||
@ -68,11 +68,11 @@ function ButtonGroupSeparator({
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -80,4 +80,4 @@ export {
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
buttonGroupVariants,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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">) {
|
||||
return (
|
||||
@ -8,11 +8,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -21,11 +21,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -54,11 +54,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -78,7 +78,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -89,4 +89,4 @@ export {
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,45 +1,45 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
} from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
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({
|
||||
@ -56,53 +56,53 @@ function Carousel({
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
if (!api) return;
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
if (!api || !setApi) return;
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
if (!api) return;
|
||||
onSelect(api);
|
||||
api.on("reInit", onSelect);
|
||||
api.on("select", onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
api?.off("select", onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
@ -129,11 +129,11 @@ function Carousel({
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -145,16 +145,16 @@ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel()
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -164,11 +164,11 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
@ -177,7 +177,7 @@ function CarouselPrevious({
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@ -189,7 +189,7 @@ function CarouselPrevious({
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
@ -198,7 +198,7 @@ function CarouselPrevious({
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
@ -207,7 +207,7 @@ function CarouselNext({
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@ -219,7 +219,7 @@ function CarouselNext({
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
@ -228,7 +228,7 @@ function CarouselNext({
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -238,4 +238,4 @@ export {
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
@ -22,11 +22,11 @@ function Command({
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
@ -37,10 +37,10 @@ function CommandDialog({
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
title?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
@ -57,7 +57,7 @@ function CommandDialog({
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
@ -74,12 +74,12 @@ function CommandInput({
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
@ -91,11 +91,11 @@ function CommandList({
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
@ -107,7 +107,7 @@ function CommandEmpty({
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
@ -119,11 +119,11 @@ function CommandGroup({
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
@ -136,7 +136,7 @@ function CommandSeparator({
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
@ -148,11 +148,11 @@ function CommandItem({
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
@ -164,11 +164,11 @@ function CommandShortcut({
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -181,4 +181,4 @@ export {
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,33 +1,33 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
@ -39,11 +39,11 @@ function DialogOverlay({
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
@ -52,7 +52,7 @@ function DialogContent({
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
@ -61,7 +61,7 @@ function DialogContent({
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -77,7 +77,7 @@ function DialogContent({
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -87,7 +87,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -96,11 +96,11 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
@ -113,7 +113,7 @@ function DialogTitle({
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
@ -126,7 +126,7 @@ function DialogDescription({
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -140,4 +140,4 @@ export {
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
@ -17,7 +17,7 @@ function DropdownMenuPortal({
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
@ -28,7 +28,7 @@ function DropdownMenuTrigger({
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
@ -43,12 +43,12 @@ function DropdownMenuContent({
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
@ -56,7 +56,7 @@ function DropdownMenuGroup({
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
@ -65,8 +65,8 @@ function DropdownMenuItem({
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
@ -75,11 +75,11 @@ function DropdownMenuItem({
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
@ -93,7 +93,7 @@ function DropdownMenuCheckboxItem({
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
@ -105,7 +105,7 @@ function DropdownMenuCheckboxItem({
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
@ -116,7 +116,7 @@ function DropdownMenuRadioGroup({
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
@ -129,7 +129,7 @@ function DropdownMenuRadioItem({
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -140,7 +140,7 @@ function DropdownMenuRadioItem({
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
@ -148,7 +148,7 @@ function DropdownMenuLabel({
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
@ -156,11 +156,11 @@ function DropdownMenuLabel({
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
@ -173,7 +173,7 @@ function DropdownMenuSeparator({
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
@ -185,17 +185,17 @@ function DropdownMenuShortcut({
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
@ -204,7 +204,7 @@ function DropdownMenuSubTrigger({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
@ -212,14 +212,14 @@ function DropdownMenuSubTrigger({
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
@ -231,11 +231,11 @@ function DropdownMenuSubContent({
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -254,4 +254,4 @@ export {
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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">) {
|
||||
return (
|
||||
@ -8,11 +8,11 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="empty"
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -21,11 +21,11 @@ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="empty-header"
|
||||
className={cn(
|
||||
"flex max-w-sm flex-col items-center gap-2 text-center",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMediaVariants = cva(
|
||||
@ -40,8 +40,8 @@ const emptyMediaVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function EmptyMedia({
|
||||
className,
|
||||
@ -55,7 +55,7 @@ function EmptyMedia({
|
||||
className={cn(emptyMediaVariants({ variant, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -65,7 +65,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("text-lg font-medium tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
@ -74,11 +74,11 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
data-slot="empty-description"
|
||||
className={cn(
|
||||
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -87,11 +87,11 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="empty-content"
|
||||
className={cn(
|
||||
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -101,4 +101,4 @@ export {
|
||||
EmptyDescription,
|
||||
EmptyContent,
|
||||
EmptyMedia,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
import * as React from "react";
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function HoverCard({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||
}
|
||||
|
||||
function HoverCardTrigger({
|
||||
@ -16,7 +16,7 @@ function HoverCardTrigger({
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function HoverCardContent({
|
||||
@ -33,12 +33,12 @@ function HoverCardContent({
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</HoverCardPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
@ -13,7 +13,7 @@ function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("group/item-group flex flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ItemSeparator({
|
||||
@ -27,7 +27,7 @@ function ItemSeparator({
|
||||
className={cn("my-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const itemVariants = cva(
|
||||
@ -48,8 +48,8 @@ const itemVariants = cva(
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Item({
|
||||
className,
|
||||
@ -59,7 +59,7 @@ function Item({
|
||||
...props
|
||||
}: React.ComponentProps<"div"> &
|
||||
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
const Comp = asChild ? Slot : "div";
|
||||
return (
|
||||
<Comp
|
||||
data-slot="item"
|
||||
@ -68,7 +68,7 @@ function Item({
|
||||
className={cn(itemVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const itemMediaVariants = cva(
|
||||
@ -85,8 +85,8 @@ const itemMediaVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function ItemMedia({
|
||||
className,
|
||||
@ -100,7 +100,7 @@ function ItemMedia({
|
||||
className={cn(itemMediaVariants({ variant, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -109,11 +109,11 @@ function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="item-content"
|
||||
className={cn(
|
||||
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -122,11 +122,11 @@ function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="item-title"
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
@ -136,11 +136,11 @@ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
className={cn(
|
||||
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
|
||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -150,7 +150,7 @@ function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("flex items-center gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -159,11 +159,11 @@ function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="item-header"
|
||||
className={cn(
|
||||
"flex basis-full items-center justify-between gap-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -172,11 +172,11 @@ function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="item-footer"
|
||||
className={cn(
|
||||
"flex basis-full items-center justify-between gap-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -190,4 +190,4 @@ export {
|
||||
ItemDescription,
|
||||
ItemHeader,
|
||||
ItemFooter,
|
||||
}
|
||||
};
|
||||
|
||||
@ -145,7 +145,7 @@
|
||||
|
||||
/* Border glow effect */
|
||||
.magic-bento-card--border-glow::after {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 6px;
|
||||
@ -186,7 +186,7 @@
|
||||
}
|
||||
|
||||
.particle::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
@ -15,7 +15,7 @@ function Progress({
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -25,7 +25,7 @@ function Progress({
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress }
|
||||
export { Progress };
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
@ -25,7 +25,7 @@ function ScrollArea({
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
@ -43,7 +43,7 @@ function ScrollBar({
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -52,7 +52,7 @@ function ScrollBar({
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
export { ScrollArea, ScrollBar };
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
@ -18,11 +18,11 @@ function Separator({
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
|
||||
@ -1,31 +1,31 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
@ -37,11 +37,11 @@ function SheetOverlay({
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
@ -50,7 +50,7 @@ function SheetContent({
|
||||
side = "right",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
@ -67,7 +67,7 @@ function SheetContent({
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -78,7 +78,7 @@ function SheetContent({
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -88,7 +88,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@ -98,7 +98,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
@ -111,7 +111,7 @@ function SheetTitle({
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
@ -124,7 +124,7 @@ function SheetDescription({
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -136,4 +136,4 @@ export {
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
"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> {
|
||||
/**
|
||||
* Width of the border in pixels
|
||||
* @default 1
|
||||
*/
|
||||
borderWidth?: number
|
||||
borderWidth?: number;
|
||||
/**
|
||||
* Duration of the animation in seconds
|
||||
* @default 14
|
||||
*/
|
||||
duration?: number
|
||||
duration?: number;
|
||||
/**
|
||||
* Color of the border, can be a single color or an array of colors
|
||||
* @default "#000000"
|
||||
*/
|
||||
shineColor?: string | string[]
|
||||
shineColor?: string | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,9 +55,9 @@ export function ShineBorder({
|
||||
}
|
||||
className={cn(
|
||||
"motion-safe:animate-shine pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
@ -7,7 +7,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
export { Skeleton };
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
@ -6,12 +6,12 @@ import {
|
||||
Loader2Icon,
|
||||
OctagonXIcon,
|
||||
TriangleAlertIcon,
|
||||
} from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
} from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
@ -34,7 +34,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster }
|
||||
export { Toaster };
|
||||
|
||||
@ -11,13 +11,17 @@
|
||||
}
|
||||
|
||||
.card-spotlight::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(circle at var(--mouse-x) var(--mouse-y), var(--spotlight-color), transparent 80%);
|
||||
background: radial-gradient(
|
||||
circle at var(--mouse-x) var(--mouse-y),
|
||||
var(--spotlight-color),
|
||||
transparent 80%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
pointer-events: none;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
@ -14,18 +14,18 @@ function Switch({
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
export { Switch };
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
@ -18,11 +18,11 @@ function Tabs({
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const tabsListVariants = cva(
|
||||
@ -37,8 +37,8 @@ const tabsListVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
@ -53,7 +53,7 @@ function TabsList({
|
||||
className={cn(tabsListVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
@ -68,11 +68,11 @@ function TabsTrigger({
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
|
||||
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
@ -85,7 +85,7 @@ function TabsContent({
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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",
|
||||
@ -25,8 +25,8 @@ const toggleVariants = cva(
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
@ -41,7 +41,7 @@ function Toggle({
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
export { Toggle, toggleVariants };
|
||||
|
||||
@ -23,7 +23,7 @@ export function ArtifactLink(props: AnchorHTMLAttributes<HTMLAnchorElement>) {
|
||||
<a
|
||||
{...rest}
|
||||
className={cn(
|
||||
"text-primary underline decoration-primary/30 underline-offset-2 hover:decoration-primary/60 transition-colors",
|
||||
"text-primary decoration-primary/30 hover:decoration-primary/60 underline underline-offset-2 transition-colors",
|
||||
className,
|
||||
)}
|
||||
target={target ?? (external ? "_blank" : undefined)}
|
||||
|
||||
@ -9,13 +9,13 @@ import {
|
||||
} from "@/components/ui/hover-card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function CitationLink({
|
||||
href,
|
||||
export function CitationLink({
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
...props
|
||||
}: ComponentProps<"a">) {
|
||||
const domain = extractDomain(href ?? "");
|
||||
|
||||
|
||||
// Priority: children > domain
|
||||
const childrenText =
|
||||
typeof children === "string"
|
||||
@ -48,12 +48,12 @@ export function CitationLink({
|
||||
<div className="p-3">
|
||||
<div className="space-y-1">
|
||||
{displayText && (
|
||||
<h4 className="truncate font-medium text-sm leading-tight">
|
||||
<h4 className="truncate text-sm leading-tight font-medium">
|
||||
{displayText}
|
||||
</h4>
|
||||
)}
|
||||
{href && (
|
||||
<p className="truncate break-all text-muted-foreground text-xs">
|
||||
<p className="text-muted-foreground truncate text-xs break-all">
|
||||
{href}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -63,7 +63,6 @@ export function CommandPalette() {
|
||||
|
||||
useGlobalShortcuts(shortcuts);
|
||||
|
||||
|
||||
const isMac =
|
||||
typeof navigator !== "undefined" && navigator.userAgent.includes("Mac");
|
||||
const metaKey = isMac ? "⌘" : "Ctrl+";
|
||||
@ -80,7 +79,10 @@ export function CommandPalette() {
|
||||
<CommandItem onSelect={handleNewChat}>
|
||||
<MessageSquarePlusIcon className="mr-2 h-4 w-4" />
|
||||
{t.sidebar.newChat}
|
||||
<CommandShortcut>{metaKey}{shiftKey}N</CommandShortcut>
|
||||
<CommandShortcut>
|
||||
{metaKey}
|
||||
{shiftKey}N
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleOpenSettings}>
|
||||
<SettingsIcon className="mr-2 h-4 w-4" />
|
||||
|
||||
@ -48,7 +48,10 @@ export function MarkdownContent({
|
||||
return (
|
||||
<a
|
||||
{...rest}
|
||||
className={cn("text-primary underline decoration-primary/30 underline-offset-2 hover:decoration-primary/60 transition-colors", className)}
|
||||
className={cn(
|
||||
"text-primary decoration-primary/30 hover:decoration-primary/60 underline underline-offset-2 transition-colors",
|
||||
className,
|
||||
)}
|
||||
target={target ?? (external ? "_blank" : undefined)}
|
||||
rel={rel ?? (external ? "noopener noreferrer" : undefined)}
|
||||
/>
|
||||
|
||||
@ -29,7 +29,10 @@ function getModeDescriptionKey(
|
||||
mode: AgentMode,
|
||||
): keyof Pick<
|
||||
Translations["inputBox"],
|
||||
"flashModeDescription" | "reasoningModeDescription" | "proModeDescription" | "ultraModeDescription"
|
||||
| "flashModeDescription"
|
||||
| "reasoningModeDescription"
|
||||
| "proModeDescription"
|
||||
| "ultraModeDescription"
|
||||
> {
|
||||
switch (mode) {
|
||||
case "flash":
|
||||
|
||||
@ -33,17 +33,20 @@ DeerFlow is proudly open source and distributed under the **MIT License**.
|
||||
We extend our heartfelt gratitude to the open source projects and contributors who have made DeerFlow a reality. We truly stand on the shoulders of giants.
|
||||
|
||||
### Core Frameworks
|
||||
|
||||
- **[LangChain](https://github.com/langchain-ai/langchain)**: A phenomenal framework that powers our LLM interactions and chains.
|
||||
- **[LangGraph](https://github.com/langchain-ai/langgraph)**: Enabling sophisticated multi-agent orchestration.
|
||||
- **[Next.js](https://nextjs.org/)**: A cutting-edge framework for building web applications.
|
||||
|
||||
### UI Libraries
|
||||
|
||||
- **[Shadcn](https://ui.shadcn.com/)**: Minimalistic components that power our UI.
|
||||
- **[SToneX](https://github.com/stonexer)**: For his invaluable contribution to token-by-token visual effects.
|
||||
|
||||
These outstanding projects form the backbone of DeerFlow and exemplify the transformative power of open source collaboration.
|
||||
|
||||
### Special Thanks
|
||||
|
||||
Finally, we want to express our heartfelt gratitude to the core authors of DeerFlow 1.0 and 2.0:
|
||||
|
||||
- **[Daniel Walnut](https://github.com/hetaoBackend/)**
|
||||
|
||||
@ -227,8 +227,7 @@ export function MemorySettingsPage() {
|
||||
const filterAll = t.settings.memory.filterAll ?? "All";
|
||||
const filterFacts = t.settings.memory.filterFacts ?? "Facts";
|
||||
const filterSummaries = t.settings.memory.filterSummaries ?? "Summaries";
|
||||
const noMatches =
|
||||
t.settings.memory.noMatches ?? "No matching memory found";
|
||||
const noMatches = t.settings.memory.noMatches ?? "No matching memory found";
|
||||
|
||||
const sectionGroups = memory ? buildMemorySectionGroups(memory, t) : [];
|
||||
const filteredSectionGroups = sectionGroups
|
||||
@ -295,7 +294,9 @@ export function MemorySettingsPage() {
|
||||
description={t.settings.memory.description}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="text-muted-foreground text-sm">{t.common.loading}</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t.common.loading}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div>Error: {error.message}</div>
|
||||
) : !memory ? (
|
||||
@ -408,7 +409,9 @@ export function MemorySettingsPage() {
|
||||
{formatTimeAgo(fact.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="break-words text-sm">{fact.content}</p>
|
||||
<p className="text-sm break-words">
|
||||
{fact.content}
|
||||
</p>
|
||||
<Link
|
||||
href={pathOfThread(fact.source)}
|
||||
className="text-primary text-sm underline-offset-4 hover:underline"
|
||||
@ -443,9 +446,7 @@ export function MemorySettingsPage() {
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{clearAllConfirmTitle}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{clearAllConfirmDescription}
|
||||
</DialogDescription>
|
||||
<DialogDescription>{clearAllConfirmDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
||||
@ -9,10 +9,7 @@ function getBaseOrigin() {
|
||||
|
||||
export function getBackendBaseURL() {
|
||||
if (env.NEXT_PUBLIC_BACKEND_BASE_URL) {
|
||||
return new URL(
|
||||
env.NEXT_PUBLIC_BACKEND_BASE_URL,
|
||||
getBaseOrigin(),
|
||||
)
|
||||
return new URL(env.NEXT_PUBLIC_BACKEND_BASE_URL, getBaseOrigin())
|
||||
.toString()
|
||||
.replace(/\/+$/, "");
|
||||
} else {
|
||||
|
||||
@ -281,14 +281,15 @@ export const enUS: Translations = {
|
||||
output: "Output",
|
||||
total: "Total",
|
||||
},
|
||||
|
||||
|
||||
// Shortcuts
|
||||
shortcuts: {
|
||||
searchActions: "Search actions...",
|
||||
noResults: "No results found.",
|
||||
actions: "Actions",
|
||||
keyboardShortcuts: "Keyboard Shortcuts",
|
||||
keyboardShortcutsDescription: "Navigate DeerFlow faster with keyboard shortcuts.",
|
||||
keyboardShortcutsDescription:
|
||||
"Navigate DeerFlow faster with keyboard shortcuts.",
|
||||
openCommandPalette: "Open Command Palette",
|
||||
toggleSidebar: "Toggle Sidebar",
|
||||
},
|
||||
|
||||
@ -218,7 +218,7 @@ export interface Translations {
|
||||
output: string;
|
||||
total: string;
|
||||
};
|
||||
|
||||
|
||||
// Shortcuts
|
||||
shortcuts: {
|
||||
searchActions: string;
|
||||
@ -254,16 +254,16 @@ export interface Translations {
|
||||
factDeleteConfirmTitle: string;
|
||||
factDeleteConfirmDescription: string;
|
||||
factDeleteSuccess: string;
|
||||
noFacts: string;
|
||||
summaryReadOnly: string;
|
||||
memoryFullyEmpty: string;
|
||||
factPreviewLabel: string;
|
||||
searchPlaceholder: string;
|
||||
filterAll: string;
|
||||
filterFacts: string;
|
||||
filterSummaries: string;
|
||||
noMatches: string;
|
||||
markdown: {
|
||||
noFacts: string;
|
||||
summaryReadOnly: string;
|
||||
memoryFullyEmpty: string;
|
||||
factPreviewLabel: string;
|
||||
searchPlaceholder: string;
|
||||
filterAll: string;
|
||||
filterFacts: string;
|
||||
filterSummaries: string;
|
||||
noMatches: string;
|
||||
markdown: {
|
||||
overview: string;
|
||||
userContext: string;
|
||||
work: string;
|
||||
|
||||
@ -268,7 +268,7 @@ export const zhCN: Translations = {
|
||||
output: "输出",
|
||||
total: "总计",
|
||||
},
|
||||
|
||||
|
||||
// Shortcuts
|
||||
shortcuts: {
|
||||
searchActions: "搜索操作...",
|
||||
@ -308,7 +308,8 @@ export const zhCN: Translations = {
|
||||
"这条事实会立即从记忆中删除。此操作无法撤销。",
|
||||
factDeleteSuccess: "事实已删除",
|
||||
noFacts: "还没有保存的事实。",
|
||||
summaryReadOnly: "摘要分区当前仍为只读。现在你可以清空全部记忆或删除单条事实。",
|
||||
summaryReadOnly:
|
||||
"摘要分区当前仍为只读。现在你可以清空全部记忆或删除单条事实。",
|
||||
memoryFullyEmpty: "还没有保存任何记忆。",
|
||||
factPreviewLabel: "即将删除的事实",
|
||||
searchPlaceholder: "搜索记忆",
|
||||
|
||||
@ -8,14 +8,12 @@ export async function loadMCPConfig() {
|
||||
}
|
||||
|
||||
export async function updateMCPConfig(config: MCPConfig) {
|
||||
const response = await fetch(`${getBackendBaseURL()}/api/mcp/config`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
const response = await fetch(`${getBackendBaseURL()}/api/mcp/config`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@ -10,7 +10,9 @@ async function readMemoryResponse(
|
||||
const errorData = (await response.json().catch(() => ({}))) as {
|
||||
detail?: string;
|
||||
};
|
||||
throw new Error(errorData.detail ?? `${fallbackMessage}: ${response.statusText}`);
|
||||
throw new Error(
|
||||
errorData.detail ?? `${fallbackMessage}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<UserMemory>;
|
||||
|
||||
@ -10,9 +10,7 @@ export interface TokenUsage {
|
||||
* Extract usage_metadata from an AI message if present.
|
||||
* The field is added by the backend (PR #1218) but not typed in the SDK.
|
||||
*/
|
||||
function getUsageMetadata(
|
||||
message: Message,
|
||||
): TokenUsage | null {
|
||||
function getUsageMetadata(message: Message): TokenUsage | null {
|
||||
if (message.type !== "ai") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -127,7 +127,10 @@ export function groupMessages<T>(
|
||||
|
||||
export function extractTextFromMessage(message: Message) {
|
||||
if (typeof message.content === "string") {
|
||||
return splitInlineReasoningFromAIMessage(message)?.content ?? message.content.trim();
|
||||
return (
|
||||
splitInlineReasoningFromAIMessage(message)?.content ??
|
||||
message.content.trim()
|
||||
);
|
||||
}
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
@ -167,7 +170,10 @@ function splitInlineReasoningFromAIMessage(message: Message) {
|
||||
|
||||
export function extractContentFromMessage(message: Message) {
|
||||
if (typeof message.content === "string") {
|
||||
return splitInlineReasoningFromAIMessage(message)?.content ?? message.content.trim();
|
||||
return (
|
||||
splitInlineReasoningFromAIMessage(message)?.content ??
|
||||
message.content.trim()
|
||||
);
|
||||
}
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
@ -233,8 +239,11 @@ export function extractURLFromImageURLContent(
|
||||
export function hasContent(message: Message) {
|
||||
if (typeof message.content === "string") {
|
||||
return (
|
||||
splitInlineReasoningFromAIMessage(message)?.content ?? message.content.trim()
|
||||
).length > 0;
|
||||
(
|
||||
splitInlineReasoningFromAIMessage(message)?.content ??
|
||||
message.content.trim()
|
||||
).length > 0
|
||||
);
|
||||
}
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content.length > 0;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user