Update local frontend and backend changes
This commit is contained in:
1 parent
faca2c9a2d
commit
a27206c543
35 files changed
+1756
-334
No files matched your search
@@ -674,12 +674,22 @@ export const PromptInput = ({
|
||||
useEffect(() => {
|
||||
if (!globalDrop) return;
|
||||
|
||||
// Suppress the global chat-upload handler when the drop happens inside an open
|
||||
// modal dialog or popover — those owners (e.g. the skill-install dialog) handle
|
||||
// their own file uploads and should not leak into the active chat thread.
|
||||
const isInsideModal = (target: EventTarget | null): boolean => {
|
||||
if (!(target instanceof Element)) return false;
|
||||
return target.closest('[role="dialog"], [role="alertdialog"]') !== null;
|
||||
};
|
||||
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
if (isInsideModal(e.target)) return;
|
||||
if (e.dataTransfer?.types?.includes("Files")) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
const onDrop = (e: DragEvent) => {
|
||||
if (isInsideModal(e.target)) return;
|
||||
if (e.dataTransfer?.types?.includes("Files")) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function Footer() {
|
||||
</div>
|
||||
<div className="text-muted-foreground container mb-8 flex flex-col items-center justify-center text-xs">
|
||||
<p>Licensed under MIT License</p>
|
||||
<p>© {year} CM业务中心-问答管理</p>
|
||||
<p>© {year} CM 助手</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { StarFilledIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NumberTicker } from "@/components/ui/number-ticker";
|
||||
import type { Locale } from "@/core/i18n/locale";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { env } from "@/env";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type HeaderProps = {
|
||||
@@ -30,11 +25,11 @@ export function Header({ className, homeURL }: HeaderProps) {
|
||||
>
|
||||
<div className="flex items-center gap-6">
|
||||
<a
|
||||
href={homeURL ?? "https://github.com/bytedance/deer-flow"}
|
||||
href={homeURL ?? "/"}
|
||||
target={isExternalHome ? "_blank" : "_self"}
|
||||
rel={isExternalHome ? "noopener noreferrer" : undefined}
|
||||
>
|
||||
<h1 className="font-serif text-xl">CM业务中心-问答管理</h1>
|
||||
<h1 className="font-serif text-xl">CM 助手</h1>
|
||||
</a>
|
||||
</div>
|
||||
<nav className="mr-8 ml-auto flex items-center gap-8 text-sm font-medium">
|
||||
@@ -51,77 +46,7 @@ export function Header({ className, homeURL }: HeaderProps) {
|
||||
{t.home.blog}
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="relative">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 z-0 h-full w-full rounded-full opacity-30 blur-2xl"
|
||||
style={{
|
||||
background: "linear-gradient(90deg, #ff80b5 0%, #9089fc 100%)",
|
||||
filter: "blur(16px)",
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
asChild
|
||||
className="group relative z-10"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/bytedance/deer-flow"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<GitHubLogoIcon className="size-4" />
|
||||
Star on GitHub
|
||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
||||
env.GITHUB_OAUTH_TOKEN && <StarCounter />}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<hr className="from-border/0 via-border/70 to-border/0 absolute top-16 right-0 left-0 z-10 m-0 h-px w-full border-none bg-linear-to-r" />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function StarCounter() {
|
||||
const [stars, setStars] = useState(10000);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function loadStars() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://api.github.com/repos/bytedance/deer-flow",
|
||||
{
|
||||
headers: env.GITHUB_OAUTH_TOKEN
|
||||
? {
|
||||
Authorization: `Bearer ${env.GITHUB_OAUTH_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
: {},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
if (!cancelled && typeof data?.stargazers_count === "number") {
|
||||
setStars(data.stargazers_count);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching GitHub stars:", error);
|
||||
}
|
||||
}
|
||||
|
||||
void loadStars();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StarFilledIcon className="size-4 transition-colors duration-300 group-hover:text-yellow-500" />
|
||||
<NumberTicker className="font-mono tabular-nums" value={stars} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function Hero({ className }: { className?: string }) {
|
||||
"Learn Anything",
|
||||
]}
|
||||
/>{" "}
|
||||
<div>CM业务中心-问答管理</div>
|
||||
<div>CM 助手</div>
|
||||
</h1>
|
||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
|
||||
<a
|
||||
|
||||
@@ -461,7 +461,7 @@ export default function ProgressiveSkillsAnimation() {
|
||||
<div className="border-b border-zinc-800 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-3 w-3 rounded-full bg-green-500" />
|
||||
<span className="text-sm text-zinc-400">CM业务中心-问答管理 Agent</span>
|
||||
<span className="text-sm text-zinc-400">CM 助手 Agent</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -691,7 +691,7 @@ export default function ProgressiveSkillsAnimation() {
|
||||
{/* Chat Input (decorative) */}
|
||||
<div className="border-t border-zinc-800 p-4">
|
||||
<div className="rounded-xl bg-zinc-800 px-4 py-3 text-sm text-zinc-500">
|
||||
Ask CM业务中心-问答管理 anything...
|
||||
Ask CM 助手 anything...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@ export function CaseStudySection({ className }: { className?: string }) {
|
||||
<Section
|
||||
className={className}
|
||||
title="Case Studies"
|
||||
subtitle="CM业务中心-问答管理"
|
||||
subtitle="CM 助手"
|
||||
>
|
||||
<div className="container-md mt-8 grid grid-cols-1 gap-4 px-4 md:grid-cols-2 md:px-20 lg:grid-cols-3">
|
||||
{caseStudies.map((caseStudy) => (
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
import { AuroraText } from "@/components/ui/aurora-text";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { Section } from "../section";
|
||||
|
||||
@@ -16,20 +12,9 @@ export function CommunitySection() {
|
||||
Join the Community
|
||||
</AuroraText>
|
||||
}
|
||||
subtitle="Contribute brilliant ideas to shape the future of DeerFlow. Collaborate, innovate, and make impacts."
|
||||
subtitle="Contribute brilliant ideas to shape the future of CM Assistant. Collaborate, innovate, and make impacts."
|
||||
>
|
||||
<div className="flex justify-center">
|
||||
<Button className="text-xl" size="lg" asChild>
|
||||
<Link
|
||||
href="https://github.com/bytedance/deer-flow"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<GitHubLogoIcon />
|
||||
Contribute Now
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div />
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export function SandboxSection({ className }: { className?: string }) {
|
||||
title="Agent Runtime Environment"
|
||||
subtitle={
|
||||
<p>
|
||||
We give DeerFlow a "computer", which can execute commands,
|
||||
We give CM Assistant a "computer", which can execute commands,
|
||||
manage files, and run long tasks — all in a secure Docker-based
|
||||
sandbox
|
||||
</p>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function SkillsSection({ className }: { className?: string }) {
|
||||
Agent Skills are loaded progressively — only what's needed, when
|
||||
it's needed.
|
||||
<br />
|
||||
Extend DeerFlow with your own skill files, or use our built-in
|
||||
Extend CM Assistant with your own skill files, or use our built-in
|
||||
library.
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ export function WhatsNewSection({ className }: { className?: string }) {
|
||||
return (
|
||||
<Section
|
||||
className={cn("", className)}
|
||||
title="CM业务中心-问答管理"
|
||||
subtitle="CM业务中心-问答管理"
|
||||
title="CM 助手"
|
||||
subtitle="CM 助手"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<MagicBento data={features} />
|
||||
|
||||
@@ -117,14 +117,7 @@ export function RecentChatList() {
|
||||
|
||||
const handleShare = useCallback(
|
||||
async (thread: AgentThread) => {
|
||||
// Always use Vercel URL for sharing so others can access
|
||||
const VERCEL_URL = "https://deer-flow-v2.vercel.app";
|
||||
const isLocalhost =
|
||||
window.location.hostname === "localhost" ||
|
||||
window.location.hostname === "127.0.0.1";
|
||||
// On localhost: use Vercel URL; On production: use current origin
|
||||
const baseUrl = isLocalhost ? VERCEL_URL : window.location.origin;
|
||||
const shareUrl = `${baseUrl}${pathOfThread(thread)}`;
|
||||
const shareUrl = `${window.location.origin}${pathOfThread(thread)}`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
toast.success(t.clipboard.linkCopied);
|
||||
|
||||
@@ -1,67 +1,18 @@
|
||||
/**
|
||||
* About DeerFlow markdown content. Inlined to avoid raw-loader dependency
|
||||
* About CM Assistant markdown content. Inlined to avoid raw-loader dependency
|
||||
* (Turbopack cannot resolve raw-loader for .md imports).
|
||||
*/
|
||||
export const aboutMarkdown = `# 🦌 [About DeerFlow 2.0](https://github.com/bytedance/deer-flow)
|
||||
export const aboutMarkdown = `# About CM Assistant
|
||||
|
||||
> **From Open Source, Back to Open Source**
|
||||
|
||||
DeerFlow (**D**eep **E**xploration and **E**fficient **R**esearch **Flow**) is an open-source **super agent harness** that orchestrates **sub-agents**, **memory**, and **sandboxes** to do almost anything — powered by **extensible skills**.
|
||||
CM Assistant is an internal super-agent platform that orchestrates sub-agents, memory, and sandboxes — powered by extensible skills — to research, analyze, and create on your behalf.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Core Features
|
||||
|
||||
* **Skills & Tools**: With built-in and extensible skills and tools, DeerFlow can do almost anything.
|
||||
* **Skills & Tools**: With built-in and extensible skills and tools, CM Assistant can do almost anything.
|
||||
* **Sub-Agents**: Sub-Agents help the main agent to do the tasks that are too complex to be done by the main agent.
|
||||
* **Sandbox & File System**: Safely execute code and manipulate files in the sandbox.
|
||||
* **Context Engineering**: Isolated sub-agent context, summarization to keep the context window sharp.
|
||||
* **Long-Term Memory**: Keep recording the user's profile, top of mind, and conversation history.
|
||||
|
||||
---
|
||||
|
||||
## 🌟 GitHub Repository
|
||||
|
||||

|
||||
|
||||
Explore DeerFlow on GitHub: [github.com/bytedance/deer-flow](https://github.com/bytedance/deer-flow)
|
||||
|
||||
## 🌐 Official Website
|
||||
|
||||
Visit the official website of DeerFlow: [deerflow.tech](https://deerflow.tech/)
|
||||
|
||||
## 📧 Support
|
||||
|
||||
If you have any questions or need help, please contact us at [support@deerflow.tech](mailto:support@deerflow.tech).
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
|
||||
DeerFlow is proudly open source and distributed under the **MIT License**.
|
||||
|
||||
---
|
||||
|
||||
## 🙌 Acknowledgments
|
||||
|
||||
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/)**
|
||||
- **[Henry Li](https://github.com/magiccube/)**
|
||||
|
||||
Without their vision, passion and dedication, \`DeerFlow\` would not be what it is today.
|
||||
`;
|
||||
@@ -1,55 +1,15 @@
|
||||
# 🦌 [About DeerFlow 2.0](https://github.com/bytedance/deer-flow)
|
||||
# About CM Assistant
|
||||
|
||||
> **From Open Source, Back to Open Source**
|
||||
|
||||
**DeerFlow** (**D**eep **E**xploration and **E**fficient **R**esearch **Flow**) is a community-driven SuperAgent harness that researches, codes, and creates.
|
||||
With the help of sandboxes, memories, tools and skills, it handles
|
||||
different levels of tasks that could take minutes to hours.
|
||||
CM Assistant is an internal super-agent platform that orchestrates sub-agents,
|
||||
memory, and sandboxes — powered by extensible skills — to research, analyze,
|
||||
and create on your behalf.
|
||||
|
||||
---
|
||||
|
||||
## 🌟 GitHub Repository
|
||||
## Core Features
|
||||
|
||||
Explore DeerFlow on GitHub: [github.com/bytedance/deer-flow](https://github.com/bytedance/deer-flow)
|
||||
|
||||
## 🌐 Official Website
|
||||
|
||||
Visit the official website of DeerFlow: [deerflow.tech](https://deerflow.tech/)
|
||||
|
||||
## 📧 Support
|
||||
|
||||
If you have any questions or need help, please contact us at [support@deerflow.tech](mailto:support@deerflow.tech).
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
|
||||
DeerFlow is proudly open source and distributed under the **MIT License**.
|
||||
|
||||
---
|
||||
|
||||
## 🙌 Acknowledgments
|
||||
|
||||
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/)**
|
||||
- **[Henry Li](https://github.com/magiccube/)**
|
||||
|
||||
Without their vision, passion and dedication, `DeerFlow` would not be what it is today.
|
||||
* **Skills & Tools**: Built-in and extensible skills let CM Assistant tackle a wide range of tasks.
|
||||
* **Sub-Agents**: Specialized sub-agents handle complex jobs the main agent delegates to them.
|
||||
* **Sandbox & File System**: Safely execute code and manipulate files inside an isolated sandbox.
|
||||
* **Context Engineering**: Isolated sub-agent context plus summarization keeps the context window sharp.
|
||||
* **Long-Term Memory**: Continuously records your profile, top-of-mind notes, and conversation history.
|
||||
@@ -389,7 +389,7 @@ export function MemorySettingsPage() {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
const exportedMemory = await exportMemory();
|
||||
const fileName = `deerflow-memory-${(exportedMemory.lastUpdated || new Date().toISOString()).replace(/[:.]/g, "-")}.json`;
|
||||
const fileName = `cm-assistant-memory-${(exportedMemory.lastUpdated || new Date().toISOString()).replace(/[:.]/g, "-")}.json`;
|
||||
const blob = new Blob([JSON.stringify(exportedMemory, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { SparklesIcon } from "lucide-react";
|
||||
import { AlertCircle, CheckCircle2, FileArchive, Loader2, SparklesIcon, Upload } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -23,9 +32,16 @@ import {
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useEnableSkill, useSkills } from "@/core/skills/hooks";
|
||||
import type { ValidateSkillUploadResponse } from "@/core/skills/api";
|
||||
import {
|
||||
useEnableSkill,
|
||||
useInstallSkillUpload,
|
||||
useSkills,
|
||||
useValidateSkillUpload,
|
||||
} from "@/core/skills/hooks";
|
||||
import type { Skill } from "@/core/skills/type";
|
||||
import { env } from "@/env";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { SettingsSection } from "./settings-section";
|
||||
|
||||
@@ -58,6 +74,7 @@ function SkillSettingsList({
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const [filter, setFilter] = useState<string>("public");
|
||||
const [installOpen, setInstallOpen] = useState(false);
|
||||
const { mutate: enableSkill } = useEnableSkill();
|
||||
const filteredSkills = useMemo(
|
||||
() => skills.filter((skill) => skill.category === filter),
|
||||
@@ -78,7 +95,16 @@ function SkillSettingsList({
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setInstallOpen(true)}
|
||||
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
|
||||
>
|
||||
<Upload className="size-4" />
|
||||
{t.settings.skills.installSkill}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleCreateSkill}>
|
||||
<SparklesIcon className="size-4" />
|
||||
{t.settings.skills.createSkill}
|
||||
@@ -110,6 +136,10 @@ function SkillSettingsList({
|
||||
</ItemActions>
|
||||
</Item>
|
||||
))}
|
||||
<InstallSkillDialog
|
||||
open={installOpen}
|
||||
onClose={() => setInstallOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -133,3 +163,265 @@ function EmptySkill({ onCreateSkill }: { onCreateSkill: () => void }) {
|
||||
</Empty>
|
||||
);
|
||||
}
|
||||
|
||||
function InstallSkillDialog({
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const dialog = t.settings.skills.installDialog;
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [validation, setValidation] = useState<ValidateSkillUploadResponse | null>(null);
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const validateMutation = useValidateSkillUpload();
|
||||
const installMutation = useInstallSkillUpload();
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setFile(null);
|
||||
setValidation(null);
|
||||
setValidationError(null);
|
||||
setIsDragging(false);
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (validateMutation.isPending || installMutation.isPending) return;
|
||||
reset();
|
||||
onClose();
|
||||
}, [installMutation.isPending, onClose, reset, validateMutation.isPending]);
|
||||
|
||||
const acceptFile = useCallback(
|
||||
async (selected: File) => {
|
||||
const lower = selected.name.toLowerCase();
|
||||
if (!lower.endsWith(".zip") && !lower.endsWith(".skill")) {
|
||||
toast.error(dialog.rejectInvalidExt);
|
||||
return;
|
||||
}
|
||||
setFile(selected);
|
||||
setValidation(null);
|
||||
setValidationError(null);
|
||||
try {
|
||||
const result = await validateMutation.mutateAsync(selected);
|
||||
setValidation(result);
|
||||
} catch (err) {
|
||||
setValidationError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
},
|
||||
[dialog.rejectInvalidExt, validateMutation],
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
// Stop the global chat-upload listener on `document` from also receiving this drop.
|
||||
// React's stopPropagation alone doesn't reach native document listeners — we need to
|
||||
// halt the underlying DOM event too.
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
setIsDragging(false);
|
||||
const dropped = e.dataTransfer.files?.[0];
|
||||
if (dropped) void acceptFile(dropped);
|
||||
},
|
||||
[acceptFile],
|
||||
);
|
||||
|
||||
const onPick = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const picked = e.target.files?.[0];
|
||||
if (picked) void acceptFile(picked);
|
||||
},
|
||||
[acceptFile],
|
||||
);
|
||||
|
||||
const handleInstall = useCallback(
|
||||
async (overwrite: boolean) => {
|
||||
if (!file) return;
|
||||
try {
|
||||
const result = await installMutation.mutateAsync({ file, overwrite });
|
||||
toast.success(
|
||||
(result.overwritten ? dialog.installSuccessReplaced : dialog.installSuccess).replace(
|
||||
"{name}",
|
||||
result.skill_name,
|
||||
),
|
||||
);
|
||||
reset();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
},
|
||||
[dialog.installSuccess, dialog.installSuccessReplaced, file, installMutation, onClose, reset],
|
||||
);
|
||||
|
||||
const isPublicConflict = validation?.conflict && validation.conflict_kind === "public";
|
||||
const isCustomConflict = validation?.conflict && validation.conflict_kind === "custom";
|
||||
const canInstall = !!validation && !validationError && !isPublicConflict && !isCustomConflict;
|
||||
const canOverwrite = !!validation && !validationError && isCustomConflict;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => !o && handleClose()}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{dialog.title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
setIsDragging(true);
|
||||
}}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={onDrop}
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center gap-2 rounded-md border-2 border-dashed p-6 text-center transition-colors",
|
||||
isDragging ? "border-primary bg-accent/40" : "border-muted-foreground/30",
|
||||
)}
|
||||
>
|
||||
<FileArchive className="text-muted-foreground size-8" />
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{dialog.dropHint}
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary ml-1 underline underline-offset-4"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
{dialog.chooseFile}
|
||||
</button>
|
||||
</div>
|
||||
{file && (
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
{file.name} · {(file.size / 1024).toFixed(1)} KB
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept=".zip,.skill,application/zip"
|
||||
hidden
|
||||
onChange={onPick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{validateMutation.isPending && (
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
{dialog.validating}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{validationError && (
|
||||
<div className="text-destructive flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm">
|
||||
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
||||
<span>{validationError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{validation && !validationError && (
|
||||
<div className="flex flex-col gap-2 rounded-md border p-3 text-sm">
|
||||
<div className="flex items-center gap-2 text-emerald-600">
|
||||
<CheckCircle2 className="size-4" />
|
||||
<span className="font-medium">{validation.skill_name}</span>
|
||||
</div>
|
||||
<Field label={dialog.descriptionLabel}>
|
||||
<span className="line-clamp-3 whitespace-pre-wrap">
|
||||
{validation.description || "—"}
|
||||
</span>
|
||||
</Field>
|
||||
{validation.license && (
|
||||
<Field label={dialog.licenseLabel}>{validation.license}</Field>
|
||||
)}
|
||||
{validation.allowed_tools.length > 0 && (
|
||||
<Field label={dialog.toolsLabel}>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{validation.allowed_tools.map((tool) => (
|
||||
<Badge key={tool} variant="secondary" className="text-xs">
|
||||
{tool}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
{validation.warnings.length > 0 && (
|
||||
<Field label={dialog.warningsLabel}>
|
||||
<ul className="list-disc pl-4 text-xs">
|
||||
{validation.warnings.map((w, idx) => (
|
||||
<li key={idx}>{w}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Field>
|
||||
)}
|
||||
{isCustomConflict && (
|
||||
<div className="text-amber-600 mt-1 flex items-start gap-2 text-xs">
|
||||
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span>
|
||||
{dialog.conflictCustom.replace("{name}", validation.skill_name)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isPublicConflict && (
|
||||
<div className="text-destructive mt-1 flex items-start gap-2 text-xs">
|
||||
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span>
|
||||
{dialog.conflictPublic.replace("{name}", validation.skill_name)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={installMutation.isPending}
|
||||
>
|
||||
{dialog.cancel}
|
||||
</Button>
|
||||
{canOverwrite && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => handleInstall(true)}
|
||||
disabled={installMutation.isPending}
|
||||
>
|
||||
{installMutation.isPending && (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
)}
|
||||
{dialog.replace}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => handleInstall(false)}
|
||||
disabled={!canInstall || installMutation.isPending}
|
||||
>
|
||||
{installMutation.isPending && (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
)}
|
||||
{installMutation.isPending ? dialog.installing : dialog.install}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-[80px_1fr] gap-2">
|
||||
<span className="text-muted-foreground shrink-0 text-xs">{label}</span>
|
||||
<div className="min-w-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,9 +15,6 @@ import {
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { GithubIcon } from "./github-icon";
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
export function WorkspaceContainer({
|
||||
className,
|
||||
children,
|
||||
@@ -90,18 +87,6 @@ export function WorkspaceHeader({
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div className="pr-4">
|
||||
<Tooltip content={t.workspace.githubTooltip}>
|
||||
<a
|
||||
href="https://github.com/bytedance/deer-flow"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="opacity-75 transition hover:opacity-100"
|
||||
>
|
||||
<GithubIcon className="size-6" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user