Compare commits
7 Commits
54dd21f18b
...
c8af7349d4
| Author | SHA1 | Date |
|---|---|---|
|
|
c8af7349d4 | |
|
|
7e3901fe21 | |
|
|
32f581cf50 | |
|
|
a8d1c8367f | |
|
|
fe3d5b7f33 | |
|
|
bc20208d0f | |
|
|
5afe834b53 |
|
|
@ -105,7 +105,10 @@ class LocalContainerBackend(SandboxBackend):
|
|||
RuntimeError: If the container fails to start.
|
||||
"""
|
||||
container_name = f"{self._container_prefix}-{sandbox_id}"
|
||||
port = get_free_port(start_port=self._base_port)
|
||||
# Check host-side Docker published ports as well, because this code may
|
||||
# run inside gateway/langgraph containers while creating sibling sandbox
|
||||
# containers via host Docker socket.
|
||||
port = get_free_port(start_port=self._base_port, check_docker_host_ports=True)
|
||||
try:
|
||||
container_id = self._start_container(container_name, port, extra_mounts)
|
||||
self._ensure_user_data_permissions(container_name)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
"""Thread-safe network utilities."""
|
||||
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
|
@ -32,7 +34,33 @@ class PortAllocator:
|
|||
self._lock = threading.Lock()
|
||||
self._reserved_ports: set[int] = set()
|
||||
|
||||
def _is_port_available(self, port: int) -> bool:
|
||||
@staticmethod
|
||||
def _is_docker_host_port_in_use(port: int) -> bool:
|
||||
"""Check whether a host port is already published by Docker containers.
|
||||
|
||||
This is useful when running inside a container (e.g. gateway/langgraph)
|
||||
while creating sibling containers through the host Docker daemon.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "ps", "--format", "{{.Ports}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=5,
|
||||
)
|
||||
except Exception:
|
||||
# Fail-open to preserve previous behavior when docker CLI is unavailable.
|
||||
return False
|
||||
|
||||
pattern = re.compile(r":(\d+)->")
|
||||
for line in result.stdout.splitlines():
|
||||
for match in pattern.finditer(line):
|
||||
if int(match.group(1)) == port:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _is_port_available(self, port: int, *, check_docker_host_ports: bool = False) -> bool:
|
||||
"""Check if a port is available for binding.
|
||||
|
||||
Args:
|
||||
|
|
@ -44,6 +72,9 @@ class PortAllocator:
|
|||
if port in self._reserved_ports:
|
||||
return False
|
||||
|
||||
if check_docker_host_ports and self._is_docker_host_port_in_use(port):
|
||||
return False
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
try:
|
||||
s.bind(("localhost", port))
|
||||
|
|
@ -51,7 +82,7 @@ class PortAllocator:
|
|||
except OSError:
|
||||
return False
|
||||
|
||||
def allocate(self, start_port: int = 8080, max_range: int = 100) -> int:
|
||||
def allocate(self, start_port: int = 8080, max_range: int = 100, *, check_docker_host_ports: bool = False) -> int:
|
||||
"""Allocate an available port in a thread-safe manner.
|
||||
|
||||
This method is thread-safe. It finds an available port, marks it as reserved,
|
||||
|
|
@ -69,7 +100,7 @@ class PortAllocator:
|
|||
"""
|
||||
with self._lock:
|
||||
for port in range(start_port, start_port + max_range):
|
||||
if self._is_port_available(port):
|
||||
if self._is_port_available(port, check_docker_host_ports=check_docker_host_ports):
|
||||
self._reserved_ports.add(port)
|
||||
return port
|
||||
|
||||
|
|
@ -85,7 +116,7 @@ class PortAllocator:
|
|||
self._reserved_ports.discard(port)
|
||||
|
||||
@contextmanager
|
||||
def allocate_context(self, start_port: int = 8080, max_range: int = 100):
|
||||
def allocate_context(self, start_port: int = 8080, max_range: int = 100, *, check_docker_host_ports: bool = False):
|
||||
"""Context manager for port allocation with automatic release.
|
||||
|
||||
Args:
|
||||
|
|
@ -95,7 +126,7 @@ class PortAllocator:
|
|||
Yields:
|
||||
An available port number.
|
||||
"""
|
||||
port = self.allocate(start_port, max_range)
|
||||
port = self.allocate(start_port, max_range, check_docker_host_ports=check_docker_host_ports)
|
||||
try:
|
||||
yield port
|
||||
finally:
|
||||
|
|
@ -106,7 +137,7 @@ class PortAllocator:
|
|||
_global_port_allocator = PortAllocator()
|
||||
|
||||
|
||||
def get_free_port(start_port: int = 8080, max_range: int = 100) -> int:
|
||||
def get_free_port(start_port: int = 8080, max_range: int = 100, *, check_docker_host_ports: bool = False) -> int:
|
||||
"""Get a free port in a thread-safe manner.
|
||||
|
||||
This function uses a global port allocator to ensure that concurrent calls
|
||||
|
|
@ -123,7 +154,11 @@ def get_free_port(start_port: int = 8080, max_range: int = 100) -> int:
|
|||
Raises:
|
||||
RuntimeError: If no available port is found in the specified range.
|
||||
"""
|
||||
return _global_port_allocator.allocate(start_port, max_range)
|
||||
return _global_port_allocator.allocate(
|
||||
start_port,
|
||||
max_range,
|
||||
check_docker_host_ports=check_docker_host_ports,
|
||||
)
|
||||
|
||||
|
||||
def release_port(port: int) -> None:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
# Artifact 缩放功能文档
|
||||
|
||||
## 功能概述
|
||||
|
||||
Artifact 缩放功能允许用户在预览 Markdown 和 HTML 内容时调整缩放比例(50% - 200%),提供更灵活的阅读体验。
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── components/
|
||||
│ └── workspace/
|
||||
│ └── artifacts/
|
||||
│ └── artifact-file-detail.tsx # 缩放组件定义与使用
|
||||
└── styles/
|
||||
└── globals.css # 缩放相关 CSS 变量
|
||||
```
|
||||
|
||||
## 核心实现
|
||||
|
||||
### 1. 缩放选择器组件 (`ArtifactZoomSelector`)
|
||||
|
||||
**位置**: `artifact-file-detail.tsx`
|
||||
|
||||
```tsx
|
||||
// 缩放比例选项
|
||||
const ZOOM_LEVELS = [50, 60, 70, 80, 90, 100, 110, 120, 130, 150, 175, 200];
|
||||
|
||||
export const ArtifactZoomSelector = ({
|
||||
value = 100,
|
||||
onChange,
|
||||
}: ArtifactZoomSelectorProps) => {
|
||||
// 放大/缩小逻辑
|
||||
// 返回: [ZoomOut图标] [百分比文本] [ZoomIn图标]
|
||||
};
|
||||
```
|
||||
|
||||
**UI 设计**:
|
||||
- 白色圆角容器,带轻微阴影和模糊效果
|
||||
- 水平三元素布局:缩小按钮 | 百分比显示 | 放大按钮
|
||||
- 支持深色模式
|
||||
- 边界值时按钮自动禁用
|
||||
|
||||
### 2. CSS 变量缩放 (`globals.css`)
|
||||
|
||||
```css
|
||||
/* 缩放变量,默认为 1 (100%) */
|
||||
:root {
|
||||
--zoom-scale: 1;
|
||||
}
|
||||
|
||||
/* 字体大小使用 calc() 计算 */
|
||||
p {
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
[data-streamdown="heading-1"] {
|
||||
font-size: calc(20px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
[data-streamdown="heading-2"],
|
||||
[data-streamdown="heading-3"] {
|
||||
font-size: calc(16px * var(--zoom-scale));
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 预览组件集成 (`ArtifactFilePreview`)
|
||||
|
||||
```tsx
|
||||
export function ArtifactFilePreview({
|
||||
content,
|
||||
language,
|
||||
zoom = 100,
|
||||
}: {
|
||||
content: string;
|
||||
language: string;
|
||||
zoom?: number;
|
||||
}) {
|
||||
const zoomScale = zoom / 100;
|
||||
|
||||
// Markdown: 使用 CSS 变量
|
||||
if (language === "markdown") {
|
||||
return (
|
||||
<div style={{ "--zoom-scale": zoomScale }}>
|
||||
<Streamdown>...</Streamdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// HTML: 使用 CSS zoom 属性
|
||||
if (language === "html") {
|
||||
return <iframe style={{ zoom: zoomScale }} />;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 在 ArtifactHeader 中使用
|
||||
|
||||
```tsx
|
||||
function ArtifactFileDetail() {
|
||||
const [zoom, setZoom] = useState(100);
|
||||
|
||||
return (
|
||||
<Artifact>
|
||||
<ArtifactHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 其他控件 */}
|
||||
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||
</div>
|
||||
{/* 中间标题 */}
|
||||
{/* 右侧操作按钮 */}
|
||||
</ArtifactHeader>
|
||||
|
||||
<ArtifactContent>
|
||||
<ArtifactFilePreview
|
||||
content={content}
|
||||
language="markdown"
|
||||
zoom={zoom}
|
||||
/>
|
||||
</ArtifactContent>
|
||||
</Artifact>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 缩放级别
|
||||
|
||||
| 级别 | 比例 |
|
||||
|------|------|
|
||||
| 最小 | 50% |
|
||||
| | 60%, 70%, 80%, 90% |
|
||||
| 默认 | 100% |
|
||||
| | 110%, 120%, 130%, 150%, 175% |
|
||||
| 最大 | 200% |
|
||||
|
||||
## 技术要点
|
||||
|
||||
1. **CSS 变量方式** - 适用于 Markdown 内容,通过 `--zoom-scale` 变量控制所有字体和间距
|
||||
2. **CSS zoom 属性** - 适用于 HTML iframe,直接缩放整个内容
|
||||
3. **状态提升** - `zoom` 状态在父组件管理,通过 props 传递给预览组件
|
||||
4. **响应式** - 缩放变化时所有相关样式自动重新计算
|
||||
|
||||
## 扩展建议
|
||||
|
||||
- 可添加快捷键支持(如 `Ctrl++` / `Ctrl+-`)
|
||||
- 可支持双击重置为 100%
|
||||
- 可将缩放状态持久化到 localStorage
|
||||
|
|
@ -8,6 +8,8 @@
|
|||
"build": "next build",
|
||||
"check": "next lint && tsc --noEmit",
|
||||
"dev": "next dev --turbo",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css,json}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,css,json}\"",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||
"preview": "next build && next start",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { MessageList } from "@/components/workspace/messages";
|
|||
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||
import { Welcome } from "@/components/workspace/welcome";
|
||||
import { useArtifacts } from "@/components/workspace/artifacts";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useNotification } from "@/core/notification/hooks";
|
||||
import { useLocalSettings } from "@/core/settings";
|
||||
|
|
@ -36,6 +37,7 @@ export default function ChatPage() {
|
|||
const { t } = useI18n();
|
||||
const [settings, setSettings] = useLocalSettings();
|
||||
const [showExitDialog, setShowExitDialog] = useState(false);
|
||||
const { fullscreen } = useArtifacts();
|
||||
|
||||
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
|
||||
useSpecificChatMode();
|
||||
|
|
@ -82,28 +84,44 @@ export default function ChatPage() {
|
|||
return (
|
||||
<ThreadContext.Provider value={{ thread, isMock }}>
|
||||
<ChatBox threadId={threadId}>
|
||||
<div className="relative flex size-full min-h-0 bg-background justify-between">
|
||||
<div className="bg-background relative flex size-full min-h-0 justify-between">
|
||||
<header
|
||||
className={cn(
|
||||
"absolute top-0 right-0 left-0 z-30 grid grid-cols-3 h-[58px] px-[20px] py-[15px] shrink-0 items-center rounded-t-[20px]",
|
||||
"absolute top-0 right-0 left-0 z-30 mx-[20px] grid h-[58px] shrink-0 grid-cols-3 items-center rounded-t-[20px] border-b py-[15px]",
|
||||
isNewThread
|
||||
? "bg-background/0 backdrop-blur-none"
|
||||
: "bg-background/80 shadow-xs backdrop-blur",
|
||||
: "bg-background/80 backdrop-blur",
|
||||
)}
|
||||
>
|
||||
{/* 返回查看结果左箭头 */}
|
||||
<div className="flex w-full items-center h-full text-sm font-medium">
|
||||
<button className="bg-transparent" onClick={() => setShowExitDialog(true)}>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.5 10H13.25H15.6875H16.5M3.5 10L7.5625 6M3.5 10L7.5625 14" stroke="#666666" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<div className="flex h-full w-full items-center text-sm font-medium">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-full px-[10px] py-[5px] text-sm font-medium"
|
||||
onClick={() => setShowExitDialog(true)}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 10H13.25H15.6875H16.5M3.5 10L7.5625 6M3.5 10L7.5625 14"
|
||||
stroke="#666666"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex w-full justify-center items-center h-full text-sm font-medium overflow-hidden">
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden text-sm font-medium">
|
||||
<ThreadTitle threadId={threadId} thread={thread} />
|
||||
</div>
|
||||
<div className="flex overflow-hidden justify-end items-center gap-2">
|
||||
<div className="flex items-center justify-end gap-2 overflow-hidden">
|
||||
<DevTodoList
|
||||
className="bg-white"
|
||||
todos={thread.values.todos ?? []}
|
||||
|
|
@ -111,8 +129,12 @@ export default function ChatPage() {
|
|||
!thread.values.todos || thread.values.todos.length === 0
|
||||
}
|
||||
trigger={
|
||||
<Button size="sm" variant="ghost" className="text-sm font-medium py-[5px] px-[10px] h-full">
|
||||
<ListTodoIcon className="size-4" /> Todo
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-full px-[10px] py-[5px] text-sm font-medium"
|
||||
>
|
||||
<ListTodoIcon className="size-4" /> To-dos
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
|
@ -129,18 +151,25 @@ export default function ChatPage() {
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div className="fixed right-0 bottom-3 left-0 z-30 flex justify-center px-4 pointer-events-none">
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none fixed right-0 bottom-3 left-0 z-30 flex justify-center px-4",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
fullscreen
|
||||
? "pointer-events-none translate-y-4 opacity-0"
|
||||
: "translate-y-0 opacity-100",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full pointer-events-auto",
|
||||
isNewThread && "-translate-y-[calc(50vh-96px)]",
|
||||
isNewThread
|
||||
? "max-w-(--container-width-sm)"
|
||||
: "max-w-(--container-width-md)",
|
||||
"pointer-events-auto relative w-full max-w-[720px]",
|
||||
isNewThread && "top-[-65px] -translate-y-[calc(50vh-96px)]",
|
||||
)}
|
||||
>
|
||||
<InputBox
|
||||
className={cn("bg-[#FBFAFC] w-full -translate-y-4 rounded-[20px] ")}
|
||||
className={cn(
|
||||
"w-full -translate-y-4 rounded-[20px] bg-[#FBFAFC]",
|
||||
)}
|
||||
isNewThread={isNewThread}
|
||||
threadId={threadId}
|
||||
autoFocus={isNewThread}
|
||||
|
|
@ -172,13 +201,21 @@ export default function ChatPage() {
|
|||
<p className="text-muted-foreground text-sm">
|
||||
退出后,当前会话结束并销毁,请先下载保存当前结果!
|
||||
</p>
|
||||
<DevDialogFooter >
|
||||
<Button className="w-full bg-[#f9f8fa] hover:bg-[#8E47F0] hover:text-white" variant="ghost" onClick={() => setShowExitDialog(false)}>
|
||||
<DevDialogFooter>
|
||||
<Button
|
||||
className="w-full bg-[#f9f8fa] hover:bg-[#8E47F0] hover:text-white"
|
||||
variant="ghost"
|
||||
onClick={() => setShowExitDialog(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="w-full bg-[#f9f8fa] hover:bg-[#8E47F0] hover:text-white" variant="ghost" onClick={() => setShowExitDialog(false)}>
|
||||
<Button
|
||||
className="w-full bg-[#f9f8fa] hover:bg-[#8E47F0] hover:text-white"
|
||||
variant="ghost"
|
||||
onClick={() => setShowExitDialog(false)}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</Button>
|
||||
</DevDialogFooter>
|
||||
</DevDialogContent>
|
||||
</DevDialog>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ export default function WorkspaceLayout({
|
|||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<WorkspaceSidebar />
|
||||
{/* TODO: !!!!必须注释!!!!! */}
|
||||
<WorkspaceSidebar className="" />
|
||||
<SidebarInset className="min-w-0">{children}</SidebarInset>
|
||||
</SidebarProvider>
|
||||
<Toaster position="top-center" />
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
|
|||
export const Artifact = ({ className, ...props }: ArtifactProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background flex flex-col overflow-hidden rounded-[20px] pt-[15px] px-[20px]",
|
||||
"bg-background flex min-w-(--container-width-sm) flex-col overflow-hidden rounded-[20px] px-[20px] pt-[15px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -31,7 +31,7 @@ export const ArtifactHeader = ({
|
|||
}: ArtifactHeaderProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-3 items-center mb-[20px] justify-between",
|
||||
"mb-[20px] grid grid-cols-3 items-center justify-between",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -143,8 +143,7 @@ export const ArtifactContent = ({
|
|||
className,
|
||||
...props
|
||||
}: ArtifactContentProps) => (
|
||||
<div
|
||||
className={cn("min-h-0 flex-1 overflow-auto p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<div className={cn("mb-[208px] p-4", className)} {...props} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -147,7 +147,11 @@ export const ChainOfThoughtStep = memo(
|
|||
{...props}
|
||||
>
|
||||
<div className="relative mt-0.5">
|
||||
{isValidElement(Icon) ? Icon : <Icon className="size-4" />}
|
||||
{isValidElement(Icon) ? (
|
||||
Icon
|
||||
) : (
|
||||
<Icon className="size-4 text-[#999999]" />
|
||||
)}
|
||||
<div className="bg-border absolute top-7 bottom-0 left-1/2 -mx-px w-px" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-2 overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ export const Message = ({ className, from, ...props }: MessageProps) => (
|
|||
<div
|
||||
className={cn(
|
||||
"group flex w-full flex-col gap-2 rounded-[10px] p-[20px]",
|
||||
from === "user" ? "is-user ml-auto justify-end" : "is-assistant bg-[#ffffff]",
|
||||
from === "user"
|
||||
? "is-user ml-auto justify-end"
|
||||
: "is-assistant bg-[#ffffff]",
|
||||
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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -295,81 +295,112 @@ export function PromptInputAttachment({
|
|||
data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
|
||||
const isImage = mediaType === "image";
|
||||
|
||||
const attachmentLabel = filename || (isImage ? "Image" : "Attachment");
|
||||
const truncateFilename = (name: string, maxLen: number = 10) => {
|
||||
if (name.length <= maxLen) return name;
|
||||
const ext = name.slice(name.lastIndexOf("."));
|
||||
const baseName = name.slice(0, name.lastIndexOf("."));
|
||||
const truncated = baseName.slice(0, maxLen - ext.length - 3);
|
||||
return truncated + "..." + ext;
|
||||
};
|
||||
|
||||
return (
|
||||
<PromptInputHoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"group border-border hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 relative flex h-8 cursor-pointer items-center gap-1.5 rounded-md border px-1.5 text-sm font-medium transition-all select-none",
|
||||
className,
|
||||
)}
|
||||
key={data.id}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative size-5 shrink-0">
|
||||
<div className="bg-background absolute inset-0 flex size-5 items-center justify-center overflow-hidden rounded transition-opacity group-hover:opacity-0">
|
||||
{isImage ? (
|
||||
<img
|
||||
alt={filename || "attachment"}
|
||||
className="size-5 object-cover"
|
||||
height={20}
|
||||
src={data.url}
|
||||
width={20}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-muted-foreground flex size-5 items-center justify-center">
|
||||
<PaperclipIcon className="size-3" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
<div
|
||||
className={cn(
|
||||
"group relative flex size-16 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-lg transition-all select-none",
|
||||
isImage ? "p-0" : "bg-gray-100 dark:bg-gray-700",
|
||||
className,
|
||||
)}
|
||||
key={data.id}
|
||||
{...props}
|
||||
>
|
||||
{isImage ? (
|
||||
<>
|
||||
<img
|
||||
alt={filename || "attachment"}
|
||||
className="size-full object-cover"
|
||||
src={data.url}
|
||||
/>
|
||||
{/* 悬浮遮罩层 */}
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100"
|
||||
style={{ borderRadius: "10px", background: "rgba(0, 0, 0, 0.60)" }}
|
||||
>
|
||||
{/* 眼睛图标 - 居中 */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M10 4.75C13.3315 4.75 16.4669 6.61444 18.9805 9.88281C19.0335 9.95183 19.0335 10.0482 18.9805 10.1172C16.4669 13.3856 13.3315 15.25 10 15.25C6.66835 15.2499 3.53309 13.3857 1.01953 10.1172C0.966466 10.0482 0.966465 9.95182 1.01953 9.88281C3.53309 6.61435 6.66835 4.75014 10 4.75Z"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M10 7.75C11.2426 7.75 12.25 8.75736 12.25 10C12.25 11.2426 11.2426 12.25 10 12.25C8.75736 12.25 7.75 11.2426 7.75 10C7.75 8.75736 8.75736 7.75 10 7.75Z"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
{/* 删除按钮 - 右上角 */}
|
||||
<button
|
||||
aria-label="Remove attachment"
|
||||
className="absolute inset-0 size-5 cursor-pointer rounded p-0 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 [&>svg]:size-2.5"
|
||||
className="absolute top-1.5 right-1.5 z-10 flex size-4 cursor-pointer items-center justify-center rounded-sm transition-colors hover:bg-white/20"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
attachments.remove(data.id);
|
||||
}}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Remove</span>
|
||||
</Button>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="8"
|
||||
height="8"
|
||||
viewBox="0 0 8 8"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M0.75 0.75L6.74995 6.74995"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M6.75 0.75L0.750025 6.74992"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span className="flex-1 truncate">{attachmentLabel}</span>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<PromptInputHoverCardContent className="w-auto p-2">
|
||||
<div className="w-auto space-y-3">
|
||||
{isImage && (
|
||||
<div className="flex max-h-96 w-96 items-center justify-center overflow-hidden rounded-md border">
|
||||
<img
|
||||
alt={filename || "attachment preview"}
|
||||
className="max-h-full max-w-full object-contain"
|
||||
height={384}
|
||||
src={data.url}
|
||||
width={448}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="min-w-0 flex-1 space-y-1 px-0.5">
|
||||
<h4 className="truncate text-sm leading-none font-semibold">
|
||||
{filename || (isImage ? "Image" : "Attachment")}
|
||||
</h4>
|
||||
{data.mediaType && (
|
||||
<p className="text-muted-foreground truncate font-mono text-xs">
|
||||
{data.mediaType}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col items-center justify-center gap-1 px-1">
|
||||
<PaperclipIcon className="size-6 text-gray-400" />
|
||||
<span className="max-w-full truncate text-center text-[10px] text-gray-500">
|
||||
{truncateFilename(filename)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</PromptInputHoverCardContent>
|
||||
</PromptInputHoverCard>
|
||||
{/* 关闭按钮 - 右上角 */}
|
||||
<button
|
||||
aria-label="Remove attachment"
|
||||
className="absolute top-1 right-1 z-10 flex size-5 cursor-pointer items-center justify-center rounded bg-white/90 transition-colors hover:bg-white dark:bg-gray-800/90 dark:hover:bg-gray-800"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
attachments.remove(data.id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<XIcon className="size-3 text-gray-600 dark:text-gray-300" />
|
||||
<span className="sr-only">Remove</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -393,13 +424,14 @@ export function PromptInputAttachments({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex w-full flex-wrap items-center gap-2 p-3", className)}
|
||||
className={cn(
|
||||
"inline-flex flex-row flex-nowrap items-center gap-2 rounded-xl p-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{attachments.files.map((file) => (
|
||||
<Fragment key={file.id}>
|
||||
<div className="max-w-60">{children(file)}</div>
|
||||
</Fragment>
|
||||
<Fragment key={file.id}>{children(file)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1032,31 +1064,57 @@ export const PromptInputSubmit = ({
|
|||
variant = "default",
|
||||
size = "sm",
|
||||
status,
|
||||
disabled,
|
||||
children,
|
||||
...props
|
||||
}: PromptInputSubmitProps) => {
|
||||
const controller = useOptionalPromptInputController();
|
||||
|
||||
// 判断是否有内容可发送
|
||||
const hasContent = controller
|
||||
? controller.textInput.value.trim().length > 0 ||
|
||||
controller.attachments.files.length > 0
|
||||
: false;
|
||||
|
||||
// 正在 streaming 时不允许发送
|
||||
// const isStreaming = status === "streaming" || status === "submitted";
|
||||
|
||||
// const isDisabled = disabled || !hasContent || isStreaming;
|
||||
|
||||
let Icon = <ArrowUpIcon className="size-4" />;
|
||||
|
||||
let text: string = "发送";
|
||||
|
||||
if (status === "submitted") {
|
||||
Icon = <Loader2Icon className="size-4 animate-spin" />;
|
||||
text = "生成中...";
|
||||
} else if (status === "streaming") {
|
||||
Icon = <SquareIcon className="size-4" />;
|
||||
text = "停止";
|
||||
} else if (status === "error") {
|
||||
Icon = <XIcon className="size-4" />;
|
||||
text = "错误";
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGroupButton
|
||||
aria-label="Submit"
|
||||
// 被button{bgc:#fff}覆盖了,只能加"!"
|
||||
className={cn(className,'rounded-[10px] w-[140px] h-[40px] text-[#8E47F0] font-bold !bg-[#F0E8FB] hover:!bg-[#8E47F0] hover:text-[#FFFFFF]')}
|
||||
className={cn(
|
||||
"h-[40px] w-[140px] rounded-[10px] border-0 font-bold transition-all",
|
||||
// isDisabled
|
||||
// ? "cursor-not-allowed !bg-gray-200 text-gray-400":
|
||||
"!bg-[#F0E8FB] text-[#8E47F0] hover:!bg-[#8E47F0] hover:text-[#FFFFFF]",
|
||||
className,
|
||||
)}
|
||||
size={size}
|
||||
type="submit"
|
||||
variant={variant}
|
||||
// disabled={isDisabled}
|
||||
{...props}
|
||||
>
|
||||
{children ?? Icon}
|
||||
发送
|
||||
{/* {children ?? Icon} */}
|
||||
{text}
|
||||
</InputGroupButton>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
@ -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-[20px] not-first:px-[20px] not-first:py-[15px]",
|
||||
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 DevDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dev-dialog" {...props} />
|
||||
return <DialogPrimitive.Root data-slot="dev-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DevDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dev-dialog-trigger" {...props} />
|
||||
return <DialogPrimitive.Trigger data-slot="dev-dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DevDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dev-dialog-portal" {...props} />
|
||||
return <DialogPrimitive.Portal data-slot="dev-dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DevDialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dev-dialog-close" {...props} />
|
||||
return <DialogPrimitive.Close data-slot="dev-dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DevDialogOverlay({
|
||||
|
|
@ -39,11 +39,11 @@ function DevDialogOverlay({
|
|||
data-slot="dev-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 DevDialogContent({
|
||||
|
|
@ -52,7 +52,7 @@ function DevDialogContent({
|
|||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DevDialogPortal data-slot="dev-dialog-portal">
|
||||
|
|
@ -60,8 +60,8 @@ function DevDialogContent({
|
|||
<DialogPrimitive.Content
|
||||
data-slot="dev-dialog-content"
|
||||
className={cn(
|
||||
"bg-[#ffffff] 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-[400px] max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-[40px] shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||
className
|
||||
"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-[400px] max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-[#ffffff] p-[40px] shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -77,7 +77,7 @@ function DevDialogContent({
|
|||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DevDialogPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DevDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -87,7 +87,7 @@ function DevDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DevDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -96,12 +96,12 @@ function DevDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="dev-dialog-footer"
|
||||
className={cn(
|
||||
// sm:justify-end
|
||||
"grid grid-cols-2 w-full gap-[30px] justify-between sm:flex-row ",
|
||||
className
|
||||
"grid w-full grid-cols-2 justify-between gap-[30px] sm:flex-row",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DevDialogTitle({
|
||||
|
|
@ -114,7 +114,7 @@ function DevDialogTitle({
|
|||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DevDialogDescription({
|
||||
|
|
@ -127,7 +127,7 @@ function DevDialogDescription({
|
|||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -141,4 +141,4 @@ export {
|
|||
DevDialogPortal,
|
||||
DevDialogTitle,
|
||||
DevDialogTrigger,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
@ -30,7 +30,7 @@ function DropdownMenuTrigger({
|
|||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
|
|
@ -45,12 +45,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-[20px] border p-[20px] shadow-md",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
|
|
@ -58,7 +58,7 @@ function DropdownMenuGroup({
|
|||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
|
|
@ -67,8 +67,8 @@ function DropdownMenuItem({
|
|||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
|
|
@ -77,11 +77,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({
|
||||
|
|
@ -95,7 +95,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}
|
||||
|
|
@ -107,7 +107,7 @@ function DropdownMenuCheckboxItem({
|
|||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
|
|
@ -118,7 +118,7 @@ function DropdownMenuRadioGroup({
|
|||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
|
|
@ -131,7 +131,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}
|
||||
>
|
||||
|
|
@ -142,7 +142,7 @@ function DropdownMenuRadioItem({
|
|||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
|
|
@ -150,7 +150,7 @@ function DropdownMenuLabel({
|
|||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
|
|
@ -158,11 +158,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({
|
||||
|
|
@ -175,7 +175,7 @@ function DropdownMenuSeparator({
|
|||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
|
|
@ -187,17 +187,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({
|
||||
|
|
@ -206,7 +206,7 @@ function DropdownMenuSubTrigger({
|
|||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
|
|
@ -214,14 +214,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({
|
||||
|
|
@ -233,11 +233,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-[20px] border p-1 shadow-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -256,4 +256,4 @@ export {
|
|||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,49 +1,55 @@
|
|||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export interface DropdownSelectorOption<T extends string> {
|
||||
value: T;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface DropdownSelectorProps<T extends string> {
|
||||
value: T;
|
||||
options: DropdownSelectorOption<T>[];
|
||||
onChange: (value: T) => void;
|
||||
triggerClassName?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
export function DropdownSelector<T extends string>({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
triggerClassName,
|
||||
contentClassName,
|
||||
}: DropdownSelectorProps<T>) {
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className={triggerClassName ?? "border-none bg-transparent shadow-none select-none focus:outline-none"}
|
||||
>
|
||||
{selectedOption?.label ?? value}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className={contentClassName}>
|
||||
<DropdownMenuRadioGroup value={value} onValueChange={(v) => onChange(v as T)}>
|
||||
{options.map((option) => (
|
||||
<DropdownMenuRadioItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export interface DropdownSelectorOption<T extends string> {
|
||||
value: T;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface DropdownSelectorProps<T extends string> {
|
||||
value: T;
|
||||
options: DropdownSelectorOption<T>[];
|
||||
onChange: (value: T) => void;
|
||||
triggerClassName?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
export function DropdownSelector<T extends string>({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
triggerClassName,
|
||||
contentClassName,
|
||||
}: DropdownSelectorProps<T>) {
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className={
|
||||
triggerClassName ??
|
||||
"border-none bg-transparent shadow-none select-none focus:outline-none"
|
||||
}
|
||||
>
|
||||
{selectedOption?.label ?? value}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className={contentClassName}>
|
||||
<DropdownMenuRadioGroup
|
||||
value={value}
|
||||
onValueChange={(v) => onChange(v as T)}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<DropdownMenuRadioItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"group/input-group overflow-hidden border-input/50 dark:bg-background/80 relative flex w-full items-center rounded-md border transition-[color,box-shadow] outline-none",
|
||||
"group/input-group border-input/50 dark:bg-background/80 relative flex w-full items-center overflow-hidden rounded-md border transition-[color,box-shadow] outline-none",
|
||||
"h-9 min-w-0 has-[>textarea]:h-auto",
|
||||
|
||||
// Variants based on alignment.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ function SidebarProvider({
|
|||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full m-auto rounded-t-[20px] overflow-hidden",
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar m-auto flex min-h-svh w-full overflow-hidden rounded-t-[20px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -207,7 +207,7 @@ function Sidebar({
|
|||
|
||||
return (
|
||||
<div
|
||||
// !
|
||||
// !
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -7,8 +7,18 @@ import {
|
|||
PackageIcon,
|
||||
SquareArrowOutUpRightIcon,
|
||||
XIcon,
|
||||
type LucideIcon,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type HTMLAttributes,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
|
|
@ -48,7 +58,8 @@ export function ArtifactFileDetail({
|
|||
threadId: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { artifacts, setOpen, select } = useArtifacts();
|
||||
const { artifacts, setOpen, select, fullscreen, setFullscreen } =
|
||||
useArtifacts();
|
||||
const isWriteFile = useMemo(() => {
|
||||
return filepathFromProps.startsWith("write-file:");
|
||||
}, [filepathFromProps]);
|
||||
|
|
@ -94,7 +105,35 @@ export function ArtifactFileDetail({
|
|||
|
||||
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [zoom, setZoom] = useState(100);
|
||||
const { isMock } = useThread();
|
||||
|
||||
// 全屏切换处理
|
||||
const handleFullscreenToggle = useCallback(() => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch((err) => {
|
||||
console.error("无法进入全屏模式:", err);
|
||||
});
|
||||
setFullscreen(true);
|
||||
} else {
|
||||
document.exitFullscreen().catch((err) => {
|
||||
console.error("无法退出全屏模式:", err);
|
||||
});
|
||||
setFullscreen(false);
|
||||
}
|
||||
}, [setFullscreen]);
|
||||
|
||||
// 监听全屏变化
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setFullscreen(!!document.fullscreenElement);
|
||||
};
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||
};
|
||||
}, [setFullscreen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSupportPreview) {
|
||||
setViewMode("preview");
|
||||
|
|
@ -127,7 +166,7 @@ export function ArtifactFileDetail({
|
|||
return (
|
||||
<Artifact className={cn(className)}>
|
||||
<ArtifactHeader>
|
||||
<div className="flex items-center gap-2 justify-start">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
{isSupportPreview && (
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
|
|
@ -162,9 +201,11 @@ export function ArtifactFileDetail({
|
|||
)}
|
||||
</ArtifactTitle>
|
||||
</div>
|
||||
<div className="flex justify-end items-center gap-2">
|
||||
<div className="flex items-center justify-end overflow-hidden">
|
||||
{/* 放大缩小选择器 */}
|
||||
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||
<ArtifactActions>
|
||||
{!isWriteFile && filepath.endsWith(".skill") && (
|
||||
{/* {!isWriteFile && filepath.endsWith(".skill") && (
|
||||
<Tooltip content={t.toolCalls.skillInstallTooltip}>
|
||||
<ArtifactAction
|
||||
icon={isInstalling ? LoaderIcon : PackageIcon}
|
||||
|
|
@ -177,8 +218,9 @@ export function ArtifactFileDetail({
|
|||
onClick={handleInstallSkill}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isWriteFile && (
|
||||
)} */}
|
||||
{/* 新界面打开的按钮 */}
|
||||
{/* {!isWriteFile && (
|
||||
<a href={urlOfArtifact({ filepath, threadId })} target="_blank">
|
||||
<ArtifactAction
|
||||
icon={SquareArrowOutUpRightIcon}
|
||||
|
|
@ -186,10 +228,10 @@ export function ArtifactFileDetail({
|
|||
tooltip={t.common.openInNewWindow}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
)} */}
|
||||
{/* 复制按钮 */}
|
||||
{isCodeFile && (
|
||||
<ArtifactAction
|
||||
icon={CopyIcon}
|
||||
label={t.clipboard.copyToClipboard}
|
||||
disabled={!content}
|
||||
onClick={async () => {
|
||||
|
|
@ -202,42 +244,162 @@ export function ArtifactFileDetail({
|
|||
}
|
||||
}}
|
||||
tooltip={t.clipboard.copyToClipboard}
|
||||
/>
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 2H13C14.1046 2 15 2.89543 15 4V13"
|
||||
stroke="#666666"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<rect
|
||||
x="2.5"
|
||||
y="4.5"
|
||||
width="10"
|
||||
height="11"
|
||||
rx="1.5"
|
||||
stroke="#666666"
|
||||
/>
|
||||
</svg>
|
||||
</ArtifactAction>
|
||||
)}
|
||||
{/* 下载按钮 */}
|
||||
{!isWriteFile && (
|
||||
<a
|
||||
href={urlOfArtifact({ filepath, threadId, download: true })}
|
||||
target="_blank"
|
||||
>
|
||||
<ArtifactAction
|
||||
icon={DownloadIcon}
|
||||
label={t.common.download}
|
||||
tooltip={t.common.download}
|
||||
/>
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16 9V14C16 15.1046 15.1046 16 14 16H4C2.89543 16 2 15.1046 2 14V9"
|
||||
stroke="#666666"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M9 2V13M9 13L5 9M9 13L13 9"
|
||||
stroke="#666666"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</ArtifactAction>
|
||||
</a>
|
||||
)}
|
||||
{/* 全屏按钮 */}
|
||||
<ArtifactAction
|
||||
label={
|
||||
fullscreen ? t.common.closeFullScreen : t.common.fullScreen
|
||||
}
|
||||
onClick={handleFullscreenToggle}
|
||||
tooltip={
|
||||
fullscreen ? t.common.closeFullScreen : t.common.fullScreen
|
||||
}
|
||||
>
|
||||
{fullscreen ? (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 2V4C6 5.10457 5.10457 6 4 6H2"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M6 16V14C6 12.8954 5.10457 12 4 12H2"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 2V4C12 5.10457 12.8954 6 14 6H16"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 16V14C12 12.8954 12.8954 12 14 12H16"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.75 15.5H4.5C3.39543 15.5 2.5 14.6046 2.5 13.5V12.25M2.5 5.75V4.5C2.5 3.39543 3.39543 2.5 4.5 2.5H5.75M12.25 2.5H13.5C14.6046 2.5 15.5 3.39543 15.5 4.5V5.75M15.5 12.25V13.5C15.5 14.6046 14.6046 15.5 13.5 15.5H12.25"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</ArtifactAction>
|
||||
{/* 关闭按钮 */}
|
||||
<ArtifactAction
|
||||
icon={XIcon}
|
||||
label={t.common.close}
|
||||
onClick={() => setOpen(false)}
|
||||
tooltip={t.common.close}
|
||||
/>
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4 14L14 4M4 4L14 14"
|
||||
stroke="#666666"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</ArtifactAction>
|
||||
</ArtifactActions>
|
||||
</div>
|
||||
</ArtifactHeader>
|
||||
<ArtifactContent className="p-0 rounded-[20px] bg-white">
|
||||
{/* 主内容区 */}
|
||||
<ArtifactContent className="rounded-[10px] bg-white p-0">
|
||||
{isSupportPreview &&
|
||||
viewMode === "preview" &&
|
||||
(language === "markdown" || language === "html") && (
|
||||
<ArtifactFilePreview
|
||||
content={displayContent}
|
||||
language={language ?? "text"}
|
||||
zoom={zoom}
|
||||
/>
|
||||
)}
|
||||
{isCodeFile && viewMode === "code" && (
|
||||
<CodeEditor
|
||||
className="size-full resize-none rounded-none border-none"
|
||||
value={displayContent ?? ""}
|
||||
zoom={zoom}
|
||||
readonly
|
||||
/>
|
||||
)}
|
||||
|
|
@ -247,7 +409,6 @@ export function ArtifactFileDetail({
|
|||
src={urlOfArtifact({ filepath, threadId, isMock })}
|
||||
/>
|
||||
)}
|
||||
{/* <div style={{ height: `${paddingBottom}px` }} /> */}
|
||||
</ArtifactContent>
|
||||
</Artifact>
|
||||
);
|
||||
|
|
@ -256,20 +417,26 @@ export function ArtifactFileDetail({
|
|||
export function ArtifactFilePreview({
|
||||
content,
|
||||
language,
|
||||
zoom = 100,
|
||||
}: {
|
||||
content: string;
|
||||
language: string;
|
||||
zoom?: number;
|
||||
}) {
|
||||
const zoomScale = zoom / 100;
|
||||
|
||||
if (language === "markdown") {
|
||||
return (
|
||||
<div className={cn("size-full px-4")}>
|
||||
<div
|
||||
className={cn("size-full p-[20px]")}
|
||||
style={{ "--zoom-scale": zoomScale } as React.CSSProperties}
|
||||
>
|
||||
<Streamdown
|
||||
className="size-full"
|
||||
{...streamdownPlugins}
|
||||
components={{ a: CitationLink }}
|
||||
>
|
||||
{content ?? ""}
|
||||
|
||||
</Streamdown>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -281,8 +448,95 @@ export function ArtifactFilePreview({
|
|||
title="Artifact preview"
|
||||
srcDoc={content}
|
||||
sandbox="allow-scripts allow-forms"
|
||||
style={{ zoom: zoomScale }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 缩放比例选项
|
||||
const ZOOM_LEVELS = [50, 60, 70, 80, 90, 100, 110, 120, 130, 150, 175, 200];
|
||||
|
||||
export type ArtifactZoomSelectorProps = Omit<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
"onChange"
|
||||
> & {
|
||||
value?: number;
|
||||
onChange?: (value: number) => void;
|
||||
};
|
||||
|
||||
export const ArtifactZoomSelector = ({
|
||||
value = 100,
|
||||
onChange,
|
||||
className,
|
||||
...props
|
||||
}: ArtifactZoomSelectorProps) => {
|
||||
const handleZoomIn = () => {
|
||||
const currentIndex = ZOOM_LEVELS.indexOf(value);
|
||||
const nextValue = ZOOM_LEVELS[currentIndex + 1];
|
||||
if (currentIndex < ZOOM_LEVELS.length - 1 && nextValue !== undefined) {
|
||||
onChange?.(nextValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleZoomOut = () => {
|
||||
const currentIndex = ZOOM_LEVELS.indexOf(value);
|
||||
const prevValue = ZOOM_LEVELS[currentIndex - 1];
|
||||
if (currentIndex > 0 && prevValue !== undefined) {
|
||||
onChange?.(prevValue);
|
||||
}
|
||||
};
|
||||
|
||||
const canZoomIn = ZOOM_LEVELS.indexOf(value) < ZOOM_LEVELS.length - 1;
|
||||
const canZoomOut = ZOOM_LEVELS.indexOf(value) > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-[10px] bg-white px-2 py-1 backdrop-blur-sm",
|
||||
"border border-gray-200/50",
|
||||
"dark:border-gray-700/50 dark:bg-gray-800/90",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleZoomIn}
|
||||
disabled={!canZoomIn}
|
||||
className={cn(
|
||||
"flex h-6 w-6 items-center justify-center rounded transition-colors",
|
||||
"text-gray-400 hover:bg-gray-100 hover:text-gray-600",
|
||||
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
||||
"dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-300",
|
||||
)}
|
||||
aria-label="放大"
|
||||
>
|
||||
<ZoomIn className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<span
|
||||
className={cn(
|
||||
"min-w-[42px] text-center text-xs font-medium text-gray-600",
|
||||
"dark:text-gray-300",
|
||||
)}
|
||||
>
|
||||
{value}%
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleZoomOut}
|
||||
disabled={!canZoomOut}
|
||||
className={cn(
|
||||
"flex h-6 w-6 items-center justify-center rounded transition-colors",
|
||||
"text-gray-400 hover:bg-gray-100 hover:text-gray-600",
|
||||
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
||||
"dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-300",
|
||||
)}
|
||||
aria-label="缩小"
|
||||
>
|
||||
<ZoomOut className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export function ArtifactFileList({
|
|||
{files.map((file) => (
|
||||
<Card
|
||||
key={file}
|
||||
className="relative cursor-pointer py-[15px] px-[20px]"
|
||||
className="relative cursor-pointer px-[20px] py-[15px]"
|
||||
onClick={() => handleClick(file)}
|
||||
>
|
||||
<CardHeader className="pr-2 pl-1">
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const ArtifactTrigger = () => {
|
|||
return (
|
||||
<Tooltip content="点击可查看生成的文件结果">
|
||||
<Button
|
||||
className="text-sm font-medium py-[5px] px-[10px] h-full"
|
||||
className="h-full px-[10px] py-[5px] text-sm font-medium"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setArtifactsOpen(true);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ export interface ArtifactsContextType {
|
|||
open: boolean;
|
||||
autoOpen: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
|
||||
fullscreen: boolean;
|
||||
setFullscreen: (fullscreen: boolean) => void;
|
||||
}
|
||||
|
||||
const ArtifactsContext = createContext<ArtifactsContextType | undefined>(
|
||||
|
|
@ -39,6 +42,7 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
|||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true",
|
||||
);
|
||||
const [autoOpen, setAutoOpen] = useState(true);
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const { setOpen: setSidebarOpen } = useSidebar();
|
||||
|
||||
const select = useCallback(
|
||||
|
|
@ -78,6 +82,9 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
|||
selectedArtifact,
|
||||
select,
|
||||
deselect,
|
||||
|
||||
fullscreen,
|
||||
setFullscreen,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from "../artifacts";
|
||||
import { useThread } from "../messages/context";
|
||||
|
||||
const FULLSCREEN_MODE = { chat: 0, artifacts: 100 };
|
||||
const CLOSE_MODE = { chat: 100, artifacts: 0 };
|
||||
const OPEN_MODE = { chat: 50, artifacts: 50 };
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|||
select: selectArtifact,
|
||||
deselect,
|
||||
selectedArtifact,
|
||||
fullscreen,
|
||||
} = useArtifacts();
|
||||
|
||||
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
||||
|
|
@ -88,13 +90,15 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|||
|
||||
useEffect(() => {
|
||||
if (layoutRef.current) {
|
||||
if (artifactPanelOpen) {
|
||||
if (fullscreen) {
|
||||
layoutRef.current.setLayout(FULLSCREEN_MODE);
|
||||
} else if (artifactPanelOpen) {
|
||||
layoutRef.current.setLayout(OPEN_MODE);
|
||||
} else {
|
||||
layoutRef.current.setLayout(CLOSE_MODE);
|
||||
}
|
||||
}
|
||||
}, [artifactPanelOpen]);
|
||||
}, [artifactPanelOpen, fullscreen]);
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup
|
||||
|
|
@ -102,25 +106,33 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|||
defaultLayout={{ chat: 100, artifacts: 0 }}
|
||||
groupRef={layoutRef}
|
||||
>
|
||||
<ResizablePanel className="relative overflow-hidden rounded-t-[20px] " defaultSize={100} id="chat">
|
||||
<ResizablePanel
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-t-[20px] transition-opacity duration-300 ease-in-out",
|
||||
fullscreen && "pointer-events-none opacity-0",
|
||||
)}
|
||||
defaultSize={100}
|
||||
id="chat"
|
||||
>
|
||||
{children}
|
||||
</ResizablePanel>
|
||||
<ResizableHandle
|
||||
className={cn(
|
||||
"opacity-33 hover:opacity-100",
|
||||
"opacity-33 transition-opacity duration-300 hover:opacity-100",
|
||||
!artifactPanelOpen && "pointer-events-none opacity-0",
|
||||
fullscreen && "pointer-events-none opacity-0",
|
||||
)}
|
||||
/>
|
||||
<ResizablePanel
|
||||
className={cn(
|
||||
"transition-all duration-300 ease-in-out ml-[20px]",
|
||||
"ml-[20px] transition-all duration-300 ease-in-out",
|
||||
!artifactsOpen && "opacity-0",
|
||||
)}
|
||||
id="artifacts"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full transition-transform duration-300 ease-in-out bg-background rounded-t-[20px]",
|
||||
"bg-background h-full rounded-t-[20px] transition-transform duration-300 ease-in-out",
|
||||
artifactPanelOpen ? "translate-x-0" : "translate-x-full",
|
||||
)}
|
||||
>
|
||||
|
|
@ -151,19 +163,22 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|||
/>
|
||||
) : (
|
||||
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4 pt-8">
|
||||
<header className="shrink-0 flex justify-center">
|
||||
<header className="flex shrink-0 justify-center">
|
||||
{/* 遍历thread.values.artifacts选择器*/}
|
||||
{thread.values.artifacts && thread.values.artifacts.length > 0 && (
|
||||
<DropdownSelector
|
||||
value={selectedArtifact ?? thread.values.artifacts[0]!}
|
||||
options={thread.values.artifacts.map((artifact) => ({
|
||||
value: artifact,
|
||||
label: getFileName(artifact),
|
||||
}))}
|
||||
onChange={selectArtifact}
|
||||
triggerClassName="text-lg font-medium bg-transparent"
|
||||
/>
|
||||
)}
|
||||
{thread.values.artifacts &&
|
||||
thread.values.artifacts.length > 0 && (
|
||||
<DropdownSelector
|
||||
value={
|
||||
selectedArtifact ?? thread.values.artifacts[0]!
|
||||
}
|
||||
options={thread.values.artifacts.map((artifact) => ({
|
||||
value: artifact,
|
||||
label: getFileName(artifact),
|
||||
}))}
|
||||
onChange={selectArtifact}
|
||||
triggerClassName="text-lg font-medium bg-transparent"
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
<main className="min-h-0 grow">
|
||||
<ArtifactFileList
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export function CodeEditor({
|
|||
disabled,
|
||||
autoFocus,
|
||||
settings,
|
||||
zoom = 100,
|
||||
}: {
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
|
|
@ -50,6 +51,7 @@ export function CodeEditor({
|
|||
disabled?: boolean;
|
||||
autoFocus?: boolean;
|
||||
settings?: unknown;
|
||||
zoom?: number;
|
||||
}) {
|
||||
const {
|
||||
thread: { isLoading },
|
||||
|
|
@ -69,13 +71,14 @@ export function CodeEditor({
|
|||
python(),
|
||||
];
|
||||
}, []);
|
||||
|
||||
const zoomScale = zoom / 100;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex cursor-text flex-col overflow-hidden rounded-md",
|
||||
className,
|
||||
)}
|
||||
style={{ "--zoom-scale": zoomScale } as React.CSSProperties}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Textarea
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"use client";
|
||||
|
||||
|
||||
import type { Todo } from "@/core/todos";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -30,11 +29,15 @@ export function DevTodoList({
|
|||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
console.log(todos);
|
||||
console.log(todos);
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className={cn("bg-white z-[100]", className)} align="start" side="top">
|
||||
<DropdownMenuContent
|
||||
className={cn("z-[100] bg-white", className)}
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<QueueList className="w-64">
|
||||
{todos.map((todo, i) => (
|
||||
<QueueItem key={i + (todo.content ?? "")}>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function FlipDisplay({
|
|||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="overflow-ellipsis overflow-hidden whitespace-nowrap"
|
||||
className="overflow-hidden overflow-ellipsis whitespace-nowrap"
|
||||
// transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -149,8 +149,10 @@ export function InputBox({
|
|||
const { models } = useModels();
|
||||
const { thread, isMock } = useThread();
|
||||
const { textInput } = usePromptInputController();
|
||||
const iframeSkill = useIframeSkill();
|
||||
const promptRootRef = useRef<HTMLDivElement | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const attachments = usePromptInputAttachments();
|
||||
|
||||
const [followups, setFollowups] = useState<string[]>([]);
|
||||
const [followupsHidden, setFollowupsHidden] = useState(false);
|
||||
|
|
@ -173,7 +175,10 @@ export function InputBox({
|
|||
if (!isFocused) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsFocused(false);
|
||||
onFocusChange?.(false);
|
||||
}
|
||||
|
|
@ -243,7 +248,14 @@ export function InputBox({
|
|||
onContextChange?.({
|
||||
...context,
|
||||
mode: getResolvedMode(mode, supportThinking),
|
||||
reasoning_effort: mode === "ultra" ? "high" : mode === "pro" ? "medium" : mode === "thinking" ? "low" : "minimal",
|
||||
reasoning_effort:
|
||||
mode === "ultra"
|
||||
? "high"
|
||||
: mode === "pro"
|
||||
? "medium"
|
||||
: mode === "thinking"
|
||||
? "low"
|
||||
: "minimal",
|
||||
});
|
||||
},
|
||||
[onContextChange, context, supportThinking],
|
||||
|
|
@ -317,7 +329,9 @@ export function InputBox({
|
|||
return;
|
||||
}
|
||||
const current = (textInput.value ?? "").trim();
|
||||
const next = current ? `${current}\n${pendingSuggestion}` : pendingSuggestion;
|
||||
const next = current
|
||||
? `${current}\n${pendingSuggestion}`
|
||||
: pendingSuggestion;
|
||||
textInput.setInput(next);
|
||||
setFollowupsHidden(true);
|
||||
setConfirmOpen(false);
|
||||
|
|
@ -397,23 +411,29 @@ export function InputBox({
|
|||
}, [context.model_name, disabled, isMock, status, thread.messages, threadId]);
|
||||
|
||||
return (
|
||||
<div ref={(el) => { promptRootRef.current = el; containerRef.current = el; }} className="relative">
|
||||
<div
|
||||
ref={(el) => {
|
||||
promptRootRef.current = el;
|
||||
containerRef.current = el;
|
||||
}}
|
||||
className="relative"
|
||||
>
|
||||
{/* 附件预览区域 - 在输入框上方 */}
|
||||
<AttachmentPreviewBar />
|
||||
|
||||
{extraHeader && (
|
||||
<div className="absolute right-0 bottom-full left-0 z-30 flex items-center justify-center pb-4">
|
||||
<ExtraHeaderContainer hasAttachments={attachments.files.length > 0}>
|
||||
{extraHeader}
|
||||
</div>
|
||||
</ExtraHeaderContainer>
|
||||
)}
|
||||
{/* 输入框主容器 */}
|
||||
<PromptInput
|
||||
className={cn(
|
||||
"w-full",
|
||||
className,
|
||||
)}
|
||||
className={cn("w-full", className)}
|
||||
inputGroupClassName={cn(
|
||||
" backdrop-blur-sm rounded-[20px]",
|
||||
"border-0 backdrop-blur-sm w-[720px] rounded-[20px]",
|
||||
"transition-[height] duration-300 ease-out",
|
||||
!isNewThread && "shadow-[0_0_20px_2px_rgba(0,0,0,0.10)]",
|
||||
effectiveIsFocused ? "h-[200px]" : "h-12",
|
||||
effectiveIsFocused ? "h-[200px]" : "h-[80px]",
|
||||
)}
|
||||
disabled={disabled}
|
||||
globalDrop
|
||||
|
|
@ -421,16 +441,18 @@ export function InputBox({
|
|||
onSubmit={handleSubmit}
|
||||
{...props}
|
||||
>
|
||||
<PromptInputAttachments>
|
||||
{(attachment) => <PromptInputAttachment data={attachment} />}
|
||||
</PromptInputAttachments>
|
||||
<PromptInputBody className={cn(
|
||||
"transition-[opacity,transform] duration-300 ease-out",
|
||||
!effectiveIsFocused && "opacity-0 pointer-events-none"
|
||||
)}>
|
||||
<PromptInputBody
|
||||
className={cn(
|
||||
"transition-[opacity,transform] duration-300 ease-out",
|
||||
!effectiveIsFocused && "opacity-100",
|
||||
)}
|
||||
>
|
||||
<PromptInputTextarea
|
||||
ref={textareaRef}
|
||||
className={cn("size-full")}
|
||||
className={cn(
|
||||
"size-full",
|
||||
!effectiveIsFocused && "h-[80px] py-0 leading-20",
|
||||
)}
|
||||
disabled={disabled}
|
||||
placeholder={t.inputBox.placeholder}
|
||||
autoFocus={autoFocus}
|
||||
|
|
@ -450,11 +472,14 @@ export function InputBox({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
<PromptInputFooter className={cn(
|
||||
"flex transition-all duration-300 ease-out",
|
||||
// height和padding都为0来隐藏
|
||||
!effectiveIsFocused && "invisible h-[0px] p-[0px] opacity-0 translate-y-2 pointer-events-none"
|
||||
)}>
|
||||
<PromptInputFooter
|
||||
className={cn(
|
||||
"flex transition-all duration-300 ease-out",
|
||||
// height和padding都为0来隐藏
|
||||
!effectiveIsFocused &&
|
||||
"pointer-events-none invisible h-[0px] translate-y-2 p-[0px] opacity-0",
|
||||
)}
|
||||
>
|
||||
{/* ========== 左侧工具栏 ========== */}
|
||||
<PromptInputTools>
|
||||
{/* 附件上传按钮 */}
|
||||
|
|
@ -499,7 +524,12 @@ export function InputBox({
|
|||
</PromptInputActionMenuTrigger>
|
||||
</ModeHoverGuide> */}
|
||||
{/* Skill 选择按钮 (iframe 与宿主页通信) */}
|
||||
<IframeSkillDialogButton className="px-2!"/>
|
||||
<IframeSkillDialogButton
|
||||
className="px-2!"
|
||||
selectedSkill={iframeSkill.selectedSkill}
|
||||
openSkillDialog={iframeSkill.openSkillDialog}
|
||||
clearSkill={iframeSkill.clearSkill}
|
||||
/>
|
||||
{/* [已禁用] 模式选择下拉菜单内容 */}
|
||||
{/* <PromptInputActionMenuContent className="w-80">
|
||||
<DropdownMenuGroup>
|
||||
|
|
@ -639,10 +669,14 @@ export function InputBox({
|
|||
<PromptInputActionMenuTrigger className="gap-1! px-2!">
|
||||
<div className="text-xs font-normal">
|
||||
{t.inputBox.reasoningEffort}:
|
||||
{context.reasoning_effort === "minimal" && " " + t.inputBox.reasoningEffortMinimal}
|
||||
{context.reasoning_effort === "low" && " " + t.inputBox.reasoningEffortLow}
|
||||
{context.reasoning_effort === "medium" && " " + t.inputBox.reasoningEffortMedium}
|
||||
{context.reasoning_effort === "high" && " " + t.inputBox.reasoningEffortHigh}
|
||||
{context.reasoning_effort === "minimal" &&
|
||||
" " + t.inputBox.reasoningEffortMinimal}
|
||||
{context.reasoning_effort === "low" &&
|
||||
" " + t.inputBox.reasoningEffortLow}
|
||||
{context.reasoning_effort === "medium" &&
|
||||
" " + t.inputBox.reasoningEffortMedium}
|
||||
{context.reasoning_effort === "high" &&
|
||||
" " + t.inputBox.reasoningEffortHigh}
|
||||
</div>
|
||||
</PromptInputActionMenuTrigger>
|
||||
<PromptInputActionMenuContent className="w-70">
|
||||
|
|
@ -697,7 +731,8 @@ export function InputBox({
|
|||
</PromptInputActionMenuItem>
|
||||
<PromptInputActionMenuItem
|
||||
className={cn(
|
||||
context.reasoning_effort === "medium" || !context.reasoning_effort
|
||||
context.reasoning_effort === "medium" ||
|
||||
!context.reasoning_effort
|
||||
? "text-accent-foreground"
|
||||
: "text-muted-foreground/65",
|
||||
)}
|
||||
|
|
@ -711,7 +746,8 @@ export function InputBox({
|
|||
{t.inputBox.reasoningEffortMediumDescription}
|
||||
</div>
|
||||
</div>
|
||||
{context.reasoning_effort === "medium" || !context.reasoning_effort ? (
|
||||
{context.reasoning_effort === "medium" ||
|
||||
!context.reasoning_effort ? (
|
||||
<CheckIcon className="ml-auto size-4" />
|
||||
) : (
|
||||
<div className="ml-auto size-4" />
|
||||
|
|
@ -785,28 +821,28 @@ export function InputBox({
|
|||
</PromptInputFooter>
|
||||
{/* 移动出来 */}
|
||||
<PromptInputSubmit
|
||||
className="absolute right-3 bottom-3 z-[20] border-0"
|
||||
className="absolute right-3 bottom-5 z-[20] border-0"
|
||||
disabled={disabled}
|
||||
variant="outline"
|
||||
status={status}
|
||||
/>
|
||||
{/* TODO: 神秘空div */}
|
||||
{/* MARK: 神秘空div */}
|
||||
{/* {!isNewThread && (
|
||||
<div className="bg-background absolute right-0 -bottom-[17px] left-0 z-0 h-4"></div>
|
||||
)} */}
|
||||
</PromptInput>
|
||||
{/* 小惊喜等 */}
|
||||
{isNewThread && searchParams.get("mode") !== "skill" && (
|
||||
<div className="absolute right-0 bottom-0 left-0 z-0 flex items-center justify-center translate-y-full pt-4">
|
||||
<SuggestionList />
|
||||
</div>
|
||||
<SuggestionListContainer
|
||||
sendSelectSkill={iframeSkill.sendSelectSkill}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!disabled &&
|
||||
!isNewThread &&
|
||||
!followupsHidden &&
|
||||
(followupsLoading || followups.length > 0) && (
|
||||
<div className="absolute right-0 -top-20 left-0 z-20 flex items-center justify-center">
|
||||
<div className="absolute -top-20 right-0 left-0 z-20 flex items-center justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
{followupsLoading ? (
|
||||
<div className="text-muted-foreground bg-background/80 rounded-full border px-4 py-2 text-xs backdrop-blur-sm">
|
||||
|
|
@ -861,11 +897,27 @@ export function InputBox({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
// SuggestionList 容器
|
||||
function SuggestionListContainer({
|
||||
sendSelectSkill,
|
||||
}: {
|
||||
sendSelectSkill: (skill_id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="absolute right-0 bottom-0 left-0 z-0 flex translate-y-full items-center justify-center pt-4">
|
||||
<SuggestionList sendSelectSkill={sendSelectSkill} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 快速选择skillbutton
|
||||
function SuggestionList() {
|
||||
function SuggestionList({
|
||||
sendSelectSkill,
|
||||
}: {
|
||||
sendSelectSkill: (skill_id: string) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { textInput } = usePromptInputController();
|
||||
const { sendSelectSkill } = useIframeSkill();
|
||||
|
||||
const handleSuggestionClick = useCallback(
|
||||
(suggestion: { prompt: string; skill_id?: string }) => {
|
||||
|
|
@ -945,22 +997,43 @@ function AddAttachmentsButton({ className }: { className?: string }) {
|
|||
return (
|
||||
<Tooltip content={t.inputBox.addAttachments}>
|
||||
<PromptInputButton
|
||||
className={cn("group px-2! hover:bg-[#EAE2F5] ", className)}
|
||||
className={cn("group px-2! hover:bg-[#EAE2F5]", className)}
|
||||
onClick={() => attachments.openFileDialog()}
|
||||
>
|
||||
<svg width="18" height="15" viewBox="0 0 18 15" fill="none" xmlns="http://www.w3.org/2000/svg" className="transition-[stroke] duration-200 [&>path]:transition-[fill,stroke] [&>path]:duration-200 [&>path:first-child]:group-hover:fill-[#8E47F0] [&>path:last-child]:group-hover:stroke-[#8E47F0]">
|
||||
<path d="M7.05042 7.65254C6.9754 7.72756 6.90039 7.80257 6.90039 7.95258C6.90039 8.02759 6.9754 8.1776 7.05042 8.25262C7.20043 8.40263 7.42545 8.40263 7.57546 8.25262L8.8506 6.97747V10.7279C8.8506 10.9529 9.00061 11.1029 9.22563 11.1029C9.30065 11.1029 9.45066 11.0279 9.52567 11.0279C9.60067 10.9529 9.67568 10.8779 9.67568 10.7279V6.97747L10.9508 8.25262C11.1008 8.40263 11.3259 8.40263 11.4759 8.25262C11.5509 8.1776 11.6259 8.10259 11.6259 7.95258C11.6259 7.87757 11.5509 7.72756 11.4759 7.65254L9.52567 5.70235C9.37564 5.55234 9.15062 5.55234 9.00061 5.70235L7.05042 7.65254Z" fill="#150033"/>
|
||||
<path d="M1.12695 0.5H6.67871C6.87077 0.500077 7.01409 0.574515 7.07324 0.648438L7.09082 0.669922L8.30762 1.88672C8.6222 2.20119 9.01344 2.3681 9.44629 2.36816H16.875C17.2382 2.36842 17.5012 2.63339 17.5 2.99414V13.8848C17.5048 14.2408 17.2454 14.5056 16.8818 14.5059H1.12695C0.764649 14.5057 0.5 14.2401 0.5 13.877V1.12793C0.500049 0.810129 0.702664 0.567404 0.996094 0.511719L1.12695 0.5Z" stroke="#150033"/>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
width="18"
|
||||
height="15"
|
||||
viewBox="0 0 18 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="transition-[stroke] duration-200 [&>path]:transition-[fill,stroke] [&>path]:duration-200 [&>path:first-child]:group-hover:fill-[#8E47F0] [&>path:last-child]:group-hover:stroke-[#8E47F0]"
|
||||
>
|
||||
<path
|
||||
d="M7.05042 7.65254C6.9754 7.72756 6.90039 7.80257 6.90039 7.95258C6.90039 8.02759 6.9754 8.1776 7.05042 8.25262C7.20043 8.40263 7.42545 8.40263 7.57546 8.25262L8.8506 6.97747V10.7279C8.8506 10.9529 9.00061 11.1029 9.22563 11.1029C9.30065 11.1029 9.45066 11.0279 9.52567 11.0279C9.60067 10.9529 9.67568 10.8779 9.67568 10.7279V6.97747L10.9508 8.25262C11.1008 8.40263 11.3259 8.40263 11.4759 8.25262C11.5509 8.1776 11.6259 8.10259 11.6259 7.95258C11.6259 7.87757 11.5509 7.72756 11.4759 7.65254L9.52567 5.70235C9.37564 5.55234 9.15062 5.55234 9.00061 5.70235L7.05042 7.65254Z"
|
||||
fill="#150033"
|
||||
/>
|
||||
<path
|
||||
d="M1.12695 0.5H6.67871C6.87077 0.500077 7.01409 0.574515 7.07324 0.648438L7.09082 0.669922L8.30762 1.88672C8.6222 2.20119 9.01344 2.3681 9.44629 2.36816H16.875C17.2382 2.36842 17.5012 2.63339 17.5 2.99414V13.8848C17.5048 14.2408 17.2454 14.5056 16.8818 14.5059H1.12695C0.764649 14.5057 0.5 14.2401 0.5 13.877V1.12793C0.500049 0.810129 0.702664 0.567404 0.996094 0.511719L1.12695 0.5Z"
|
||||
stroke="#150033"
|
||||
/>
|
||||
</svg>
|
||||
</PromptInputButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
// 启动iframeSkillDialog
|
||||
function IframeSkillDialogButton({ className }: { className?: string }) {
|
||||
function IframeSkillDialogButton({
|
||||
className,
|
||||
selectedSkill,
|
||||
openSkillDialog,
|
||||
clearSkill,
|
||||
}: {
|
||||
className?: string;
|
||||
selectedSkill: { skill_id: string; title: string } | null;
|
||||
openSkillDialog: () => void;
|
||||
clearSkill: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { selectedSkill, openSkillDialog, clearSkill } = useIframeSkill();
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -969,17 +1042,28 @@ function IframeSkillDialogButton({ className }: { className?: string }) {
|
|||
className={cn("group px-2! hover:bg-[#EAE2F5]", className)}
|
||||
onClick={openSkillDialog}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="size-4 transition-[stroke] duration-200 [&>path]:transition-[stroke] [&>path]:duration-200 [&>path]:group-hover:stroke-[#8E47F0]" viewBox="0 0 12 16" fill="none">
|
||||
<path d="M3.7998 0.5H9.19922C9.24033 0.5 9.26852 0.518136 9.28516 0.541992C9.30124 0.565318 9.30411 0.588767 9.29395 0.613281H9.29297L7.43066 5.07422L7.1416 5.76758H11.3994C11.4295 5.76765 11.4474 5.77552 11.459 5.7832C11.4724 5.79207 11.4846 5.80503 11.4922 5.82129C11.4997 5.83745 11.5013 5.85253 11.5 5.86328C11.4989 5.87156 11.4953 5.88556 11.4785 5.9043L2.87891 15.4629V15.4639C2.85396 15.4914 2.83406 15.4971 2.82031 15.499C2.80144 15.5016 2.77553 15.4981 2.74902 15.4844C2.72225 15.4705 2.70837 15.453 2.70312 15.4424C2.70056 15.4372 2.69457 15.4253 2.70312 15.3936V15.3926L4.30273 9.49512L4.47461 8.86426H0.600586C0.559682 8.86424 0.531324 8.84587 0.514648 8.82227C0.498608 8.79944 0.496551 8.777 0.505859 8.75293L3.70508 0.558594C3.71075 0.544183 3.72173 0.529788 3.73828 0.518555C3.74688 0.51277 3.75704 0.508037 3.76758 0.504883L3.7998 0.5Z" stroke="#150033"/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-4 transition-[stroke] duration-200 [&>path]:transition-[stroke] [&>path]:duration-200 [&>path]:group-hover:stroke-[#8E47F0]"
|
||||
viewBox="0 0 12 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3.7998 0.5H9.19922C9.24033 0.5 9.26852 0.518136 9.28516 0.541992C9.30124 0.565318 9.30411 0.588767 9.29395 0.613281H9.29297L7.43066 5.07422L7.1416 5.76758H11.3994C11.4295 5.76765 11.4474 5.77552 11.459 5.7832C11.4724 5.79207 11.4846 5.80503 11.4922 5.82129C11.4997 5.83745 11.5013 5.85253 11.5 5.86328C11.4989 5.87156 11.4953 5.88556 11.4785 5.9043L2.87891 15.4629V15.4639C2.85396 15.4914 2.83406 15.4971 2.82031 15.499C2.80144 15.5016 2.77553 15.4981 2.74902 15.4844C2.72225 15.4705 2.70837 15.453 2.70312 15.4424C2.70056 15.4372 2.69457 15.4253 2.70312 15.3936V15.3926L4.30273 9.49512L4.47461 8.86426H0.600586C0.559682 8.86424 0.531324 8.84587 0.514648 8.82227C0.498608 8.79944 0.496551 8.777 0.505859 8.75293L3.70508 0.558594C3.71075 0.544183 3.72173 0.529788 3.73828 0.518555C3.74688 0.51277 3.75704 0.508037 3.76758 0.504883L3.7998 0.5Z"
|
||||
stroke="#150033"
|
||||
/>
|
||||
</svg>
|
||||
</PromptInputButton>
|
||||
</Tooltip>
|
||||
{selectedSkill && (
|
||||
<Badge variant="secondary" className="gap-1 px-[15px] py-[4px] text-[#8E47F0] bg-[#EAE2F5]">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="gap-1 bg-[#EAE2F5] px-[15px] py-[4px] text-[#8E47F0]"
|
||||
>
|
||||
{selectedSkill.title}
|
||||
<button
|
||||
onClick={clearSkill}
|
||||
className="ml-1 rounded-full hover:bg-muted-foreground/20"
|
||||
className="hover:bg-muted-foreground/20 ml-1 rounded-full"
|
||||
>
|
||||
<XIcon className="size-3" />
|
||||
</button>
|
||||
|
|
@ -988,3 +1072,40 @@ function IframeSkillDialogButton({ className }: { className?: string }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 附件预览栏 - 在输入框上方显示
|
||||
function AttachmentPreviewBar() {
|
||||
const attachments = usePromptInputAttachments();
|
||||
|
||||
if (!attachments.files.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-full left-0 z-20 mb-3 ml-1 flex justify-start">
|
||||
<PromptInputAttachments>
|
||||
{(attachment) => <PromptInputAttachment data={attachment} />}
|
||||
</PromptInputAttachments>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ExtraHeader 容器 - 有附件时上浮
|
||||
function ExtraHeaderContainer({
|
||||
hasAttachments,
|
||||
children,
|
||||
}: {
|
||||
hasAttachments: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-0 bottom-full left-0 z-30 flex items-center justify-center pb-4 transition-transform duration-300",
|
||||
hasAttachments && "-translate-y-20",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,7 +151,12 @@ export function MessageGroup({
|
|||
<ChainOfThoughtStep
|
||||
className="font-normal"
|
||||
label={t.common.thinking}
|
||||
icon={LightbulbIcon}
|
||||
icon={
|
||||
<LightbulbIcon
|
||||
className="size-4"
|
||||
style={{ color: "#999999" }}
|
||||
/>
|
||||
}
|
||||
></ChainOfThoughtStep>
|
||||
<div>
|
||||
<ChevronUp
|
||||
|
|
|
|||
|
|
@ -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/)**
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ export function Welcome({
|
|||
{/* <div className={cn("inline-block", !waved ? "animate-wave" : "")}>
|
||||
{isUltra ? "🚀" : "👋"}
|
||||
</div> */}
|
||||
<AuroraText className="text-[#150033] text-[18px]" colors={colors}>{t.welcome.greeting}</AuroraText>
|
||||
<AuroraText className="text-[18px] text-[#150033]" colors={colors}>
|
||||
{t.welcome.greeting}
|
||||
</AuroraText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ export const enUS: Translations = {
|
|||
delete: "Delete",
|
||||
rename: "Rename",
|
||||
share: "Share",
|
||||
fullScreen: "fullScreen",
|
||||
closeFullScreen: "closeFullScreen",
|
||||
openInNewWindow: "Open in new window",
|
||||
close: "Close",
|
||||
more: "More",
|
||||
|
|
@ -106,31 +108,36 @@ export const enUS: Translations = {
|
|||
suggestions: [
|
||||
{
|
||||
suggestion: "Paper Writing",
|
||||
prompt: "Write an academic paper about [topic], including abstract, introduction, body and references.",
|
||||
prompt:
|
||||
"Write an academic paper about [topic], including abstract, introduction, body and references.",
|
||||
icon: PenLineIcon,
|
||||
skill_id: "paper-writing",
|
||||
},
|
||||
{
|
||||
suggestion: "Report Generation",
|
||||
prompt: "Analyze [topic] in depth and generate a well-structured research report.",
|
||||
prompt:
|
||||
"Analyze [topic] in depth and generate a well-structured research report.",
|
||||
icon: MicroscopeIcon,
|
||||
skill_id: "report-generation",
|
||||
},
|
||||
{
|
||||
suggestion: "Copywriting",
|
||||
prompt: "Create a complete planning proposal and promotional copy for [project/event].",
|
||||
prompt:
|
||||
"Create a complete planning proposal and promotional copy for [project/event].",
|
||||
icon: ShapesIcon,
|
||||
skill_id: "planning-copywriting",
|
||||
},
|
||||
{
|
||||
suggestion: "PPT Generation",
|
||||
prompt: "Generate a PPT presentation outline and content about [topic].",
|
||||
prompt:
|
||||
"Generate a PPT presentation outline and content about [topic].",
|
||||
icon: GraduationCapIcon,
|
||||
skill_id: "ppt-generation",
|
||||
},
|
||||
{
|
||||
suggestion: "Document Processing",
|
||||
prompt: "Process [document] with reading, summarizing, translating or format conversion.",
|
||||
prompt:
|
||||
"Process [document] with reading, summarizing, translating or format conversion.",
|
||||
icon: CompassIcon,
|
||||
skill_id: "document-processing",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ export interface Translations {
|
|||
share: string;
|
||||
openInNewWindow: string;
|
||||
close: string;
|
||||
fullScreen: string;
|
||||
closeFullScreen: string;
|
||||
more: string;
|
||||
search: string;
|
||||
download: string;
|
||||
|
|
@ -55,7 +57,7 @@ export interface Translations {
|
|||
placeholder: string;
|
||||
createSkillPrompt: string;
|
||||
addAttachments: string;
|
||||
selectSkill:string;
|
||||
selectSkill: string;
|
||||
mode: string;
|
||||
flashMode: string;
|
||||
flashModeDescription: string;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ export const zhCN: Translations = {
|
|||
share: "分享",
|
||||
openInNewWindow: "在新窗口打开",
|
||||
close: "关闭",
|
||||
fullScreen: "全屏",
|
||||
closeFullScreen: "关闭全屏",
|
||||
more: "更多",
|
||||
search: "搜索",
|
||||
download: "下载",
|
||||
|
|
@ -70,7 +72,7 @@ export const zhCN: Translations = {
|
|||
createSkillPrompt:
|
||||
"我们一起用 skill-creator 技能来创建一个技能吧。先问问我希望这个技能能做什么。",
|
||||
addAttachments: "添加附件",
|
||||
selectSkill:"选择Skill",
|
||||
selectSkill: "选择Skill",
|
||||
mode: "模式",
|
||||
flashMode: "闪速",
|
||||
flashModeDescription: "快速且高效的完成任务,但可能不够精准",
|
||||
|
|
@ -101,7 +103,8 @@ export const zhCN: Translations = {
|
|||
suggestions: [
|
||||
{
|
||||
suggestion: "论文写作",
|
||||
prompt: "撰写一篇关于[主题]的学术论文,包含摘要、引言、正文和参考文献。",
|
||||
prompt:
|
||||
"撰写一篇关于[主题]的学术论文,包含摘要、引言、正文和参考文献。",
|
||||
icon: PenLineIcon,
|
||||
skill_id: "1",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -379,7 +379,8 @@ export function useThreads(
|
|||
// Preserve prior semantics: if a non-positive limit is explicitly provided,
|
||||
// delegate to a single search call with the original parameters.
|
||||
if (maxResults !== undefined && maxResults <= 0) {
|
||||
const response = await apiClient.threads.search<AgentThreadState>(params);
|
||||
const response =
|
||||
await apiClient.threads.search<AgentThreadState>(params);
|
||||
return response as AgentThread[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,61 +1,73 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
// 消息类型常量
|
||||
const MESSAGE_TYPES = {
|
||||
SELECT_SKILL: 'selectSkill',
|
||||
OPEN_SKILL_DIALOG: 'openSkillDialog',
|
||||
} as const;
|
||||
|
||||
// Skill 数据类型
|
||||
interface SkillData {
|
||||
skill_id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
// Hook 返回类型
|
||||
interface UseIframeSkillReturn {
|
||||
selectedSkill: SkillData | null;
|
||||
sendSelectSkill: (skill_id: string) => void;
|
||||
openSkillDialog: () => void;
|
||||
clearSkill: () => void;
|
||||
}
|
||||
|
||||
export function useIframeSkill(): UseIframeSkillReturn {
|
||||
const searchParams = useSearchParams();
|
||||
const skillIdFromQuery = searchParams.get('skill_id');
|
||||
const titleFromQuery = searchParams.get('title');
|
||||
|
||||
const [selectedSkill, setSelectedSkill] = useState<SkillData | null>(null);
|
||||
|
||||
// 监听 query 参数变化
|
||||
useEffect(() => {
|
||||
console.log('[useIframeSkill] Query params changed - skill_id:', skillIdFromQuery, 'title:', titleFromQuery);
|
||||
if (skillIdFromQuery && titleFromQuery) {
|
||||
const skill = { skill_id: skillIdFromQuery, title: titleFromQuery };
|
||||
console.log('[useIframeSkill] Setting skill from URL:', skill);
|
||||
setSelectedSkill(skill);
|
||||
}
|
||||
}, [skillIdFromQuery, titleFromQuery]);
|
||||
|
||||
// 发送选择预定义 skill
|
||||
const sendSelectSkill = useCallback((skill_id: string) => {
|
||||
const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id };
|
||||
console.log('[useIframeSkill] sendSelectSkill:', message);
|
||||
window.parent.postMessage(message, '*');
|
||||
}, []);
|
||||
|
||||
// 打开 skill 选择对话框
|
||||
const openSkillDialog = useCallback(() => {
|
||||
const message = { type: MESSAGE_TYPES.OPEN_SKILL_DIALOG, openSkillDialog: true };
|
||||
console.log('[useIframeSkill] openSkillDialog:', message);
|
||||
window.parent.postMessage(message, '*');
|
||||
}, []);
|
||||
|
||||
// 清除选中
|
||||
const clearSkill = useCallback(() => {
|
||||
setSelectedSkill(null);
|
||||
}, []);
|
||||
|
||||
return { selectedSkill, sendSelectSkill, openSkillDialog, clearSkill };
|
||||
}
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
// 消息类型常量
|
||||
const MESSAGE_TYPES = {
|
||||
SELECT_SKILL: "selectSkill",
|
||||
OPEN_SKILL_DIALOG: "openSkillDialog",
|
||||
} as const;
|
||||
|
||||
// Skill 数据类型
|
||||
interface SkillData {
|
||||
skill_id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
// Hook 返回类型
|
||||
interface UseIframeSkillReturn {
|
||||
selectedSkill: SkillData | null;
|
||||
sendSelectSkill: (skill_id: string) => void;
|
||||
openSkillDialog: () => void;
|
||||
clearSkill: () => void;
|
||||
}
|
||||
|
||||
export function useIframeSkill(): UseIframeSkillReturn {
|
||||
const searchParams = useSearchParams();
|
||||
const skillIdFromQuery = searchParams.get("skill_id");
|
||||
const titleFromQuery = searchParams.get("title");
|
||||
|
||||
const [selectedSkill, setSelectedSkill] = useState<SkillData | null>(null);
|
||||
|
||||
// 监听 query 参数变化
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
"[useIframeSkill] Query params changed - skill_id:",
|
||||
skillIdFromQuery,
|
||||
"title:",
|
||||
titleFromQuery,
|
||||
);
|
||||
if (skillIdFromQuery && titleFromQuery) {
|
||||
const skill = { skill_id: skillIdFromQuery, title: titleFromQuery };
|
||||
console.log("[useIframeSkill] Setting skill from URL:", skill);
|
||||
setSelectedSkill(skill);
|
||||
}
|
||||
}, [skillIdFromQuery, titleFromQuery]);
|
||||
|
||||
// 发送选择预定义 skill
|
||||
const sendSelectSkill = useCallback((skill_id: string) => {
|
||||
const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id };
|
||||
console.log("[useIframeSkill] sendSelectSkill:", message);
|
||||
window.parent.postMessage(message, "*");
|
||||
}, []);
|
||||
|
||||
// 打开 skill 选择对话框
|
||||
const openSkillDialog = useCallback(() => {
|
||||
const message = {
|
||||
type: MESSAGE_TYPES.OPEN_SKILL_DIALOG,
|
||||
openSkillDialog: true,
|
||||
};
|
||||
console.log("[useIframeSkill] openSkillDialog:", message);
|
||||
window.parent.postMessage(message, "*");
|
||||
}, []);
|
||||
|
||||
// 清除选中并发送 skill_id=0 给主页
|
||||
const clearSkill = useCallback(() => {
|
||||
setSelectedSkill(null);
|
||||
// 发送 skill_id=0 给主页,通知取消选择
|
||||
const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id: "0" };
|
||||
console.log("[useIframeSkill] clearSkill, sending skill_id=0:", message);
|
||||
window.parent.postMessage(message, "*");
|
||||
}, []);
|
||||
|
||||
return { selectedSkill, sendSelectSkill, openSkillDialog, clearSkill };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile
|
||||
return !!isMobile;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,8 +72,9 @@
|
|||
|
||||
@theme {
|
||||
--font-sans:
|
||||
"Microsoft YaHei", "微软雅黑", var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
"Microsoft YaHei", "微软雅黑", var(--font-geist-sans), ui-sans-serif,
|
||||
system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol", "Noto Color Emoji";
|
||||
|
||||
--animate-fade-in: fade-in 1.1s;
|
||||
@keyframes fade-in {
|
||||
|
|
@ -225,7 +226,7 @@
|
|||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: #F9F8FA;
|
||||
--background: #f9f8fa;
|
||||
--foreground: oklch(0.145 0 0);
|
||||
/* --foreground: #00000066; */
|
||||
/* --card: oklch(1 0.0098 87.47); */
|
||||
|
|
@ -237,13 +238,13 @@
|
|||
--primary: oklch(0 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
/* --secondary: oklch(0.9455 0.0098 87.47); */
|
||||
--secondary: #1500331A;
|
||||
--secondary: #1500331a;
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
/* --muted: oklch(0.97 0.0098 87.47); */
|
||||
--muted: #1500331A;
|
||||
--muted: #1500331a;
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
/* --accent: oklch(0.94 0.0098 87.47); */
|
||||
--accent: #1500331A;
|
||||
--accent: #1500331a;
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0.0098 87.47);
|
||||
|
|
@ -409,3 +410,46 @@
|
|||
--container-width-md: calc(var(--spacing) * 204);
|
||||
--container-width-lg: calc(var(--spacing) * 256);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Streamdown Markdown Styles
|
||||
使用 data-streamdown 属性选择器统一定义
|
||||
支持 zoom-scale CSS 变量进行缩放
|
||||
======================================== */
|
||||
|
||||
/* 缩放变量,默认为 1 (100%) */
|
||||
:root {
|
||||
--zoom-scale: 1;
|
||||
}
|
||||
|
||||
/* p标签没有标识data-streamdown,暂时只能这么写 */
|
||||
p {
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
/* 列表项 - 14px */
|
||||
[data-streamdown="list-item"] {
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
padding-top: calc(4px * var(--zoom-scale));
|
||||
padding-bottom: calc(4px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
/* 一级标题 - 20px */
|
||||
[data-streamdown="heading-1"] {
|
||||
font-size: calc(20px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
/* 二三级标题 - 16px */
|
||||
[data-streamdown="heading-2"],
|
||||
[data-streamdown="heading-3"] {
|
||||
font-size: calc(16px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
/* 二三级标题 - 16px */
|
||||
[data-streamdown="code-block"] pre {
|
||||
font-size: calc(16px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue